Локальный repo с четырьмя приложениями (API Gateway, Orders, Payments, Frontend) и инфраструктурой (Kafka + Zookeeper, два PostgreSQL). Основная цель — показать, как простые outbox/inbox-воркеры, идемпотентность и атомарные SQL-операции дают effectively exactly-once семантику без тяжёлых фреймворков.
api-gateway/src/ApiGateway— минимальный proxy (Minimal API + HttpClient).order-service/src/OrderService.Api+OrderService.Infrastructure— Web API, Swagger, EF Core, outbox, consumer результатов оплат.payments-service/src/PaymentsService.Api+PaymentsService.Infrastructure— счета, атомарные списания, inbox/outbox, Kafka consumer/producer.frontend— Vite + React SPA, nginx проксирует/apiна gateway.docker-compose.yml— Kafka, Zookeeper, 2×Postgres, все сервисы и фронт.tests/GoZon.Tests— пара unit-тестов на маппинг статусов/сообщений.
- Установите Docker и Docker Compose.
- Выполните
docker compose up --build. - Через ~1–2 минуты сервисы будут доступны:
- API Gateway: http://localhost:58080 (все REST вызовы идут через него)
- Orders Service: http://localhost:55000
- Payments Service: http://localhost:55001
- Frontend SPA: http://localhost:53000
- Kafka:
localhost:19092, Zookeeper:localhost:22181 - Postgres Orders: порт 55432, Postgres Payments: порт 55433
| Маршрут | Метод | Назначение |
|---|---|---|
/api/payments/accounts |
POST | Создать счёт для user_id (409, если уже есть). |
/api/payments/accounts/{userId}/topup |
POST | Пополнение, внутри UPDATE ... WHERE balance >=. |
/api/payments/accounts/{userId}/balance |
GET | Узнать баланс и updated_at. |
/api/orders |
POST | Создать заказ → в транзакции orders + outbox (order.payment.requests). |
/api/orders |
GET | Список заказов, фильтр user_id через query. |
/api/orders/{orderId} |
GET | Текущий статус (NEW/FINISHED/CANCELLED) + failureReason. |
Фронтенд вызывает только эти маршруты (через nginx-прокси /api → gateway). Внутренние сервисы напрямую извне недоступны.
- Payments: http://localhost:55001/swagger — создайте счёт (
POST /api/accounts), затемPOST /api/accounts/{userId}/topup, затемGET /api/accounts/{userId}/balance. - Orders: http://localhost:55000/swagger —
POST /api/ordersзапускает оплату;GET /api/orders/{orderId}позволяет следить за статусом. - API Gateway (http://localhost:58080/health) покажет суммарное состояние Orders/Payments.
SPA на http://localhost:53000 разделена на два блокa: управление счётом (создание, пополнение, просмотр баланса) и создание заказа. После отправки заказа клиент автоматически poll-ит /api/orders/{id} (через gateway) каждую секунду в течение 30 секунд и отображает итоговый статус/причину. Для локальной разработки npm install && npm run dev (Vite проксирует /api на http://localhost:58080).
Проект реализует Effectively Exactly-Once семантику обработки сообщений с помощью паттернов Outbox и Inbox и идемпотентных операций.
sequenceDiagram
participant C as Client (Frontend)
participant O as Order Service
participant K as Kafka
participant P as Payments Service
C->>O: POST /api/orders (Создать заказ)
Note over O: DB Transaction Start
O->>O: Сохранить Order (Status: NEW)
O->>O: Записать Outbox (order.payment.requests)
Note over O: DB Transaction Commit
O-->>C: 201 Created (Order ID)
loop Outbox Dispatcher
O->>O: Найти не отправленные Outbox сообщения
O->>K: Опубликовать в Kafka (order.payment.requests)
O->>O: Пометить Outbox как отправленный
end
loop Payment Consumer
K->>P: Сообщение: Списать X у User Y
Note over P: DB Transaction Start
P->>P: Записать Inbox (message_id) - Защита от дублей
P->>P: Атомарный UPDATE Account (WHERE balance >= X)
P->>P: Записать Payment (order_id UNIQUE) - Защита от double-charge
P->>P: Записать Outbox (order.payment.results)
Note over P: DB Transaction Commit
P->>K: Подтвердить смещение (Manual Commit Offset)
end
loop Payment Result Consumer
K->>O: Сообщение: Оплата OK/Fail
Note over O: DB Transaction Start
O->>O: Записать Inbox (message_id)
O->>O: Обновить Order Status (FINISHED/CANCELLED)
Note over O: DB Transaction Commit
end
C->>O: GET /api/orders/{id} (Polling статуса)
O-->>C: Статус заказа
- Transactional Outbox: Каждое изменение в базе (создание заказа, списание денег) происходит одновременно с записью события в таблицу
outboxв рамках одной SQL-транзакции. Это гарантирует, что мы никогда не забудем уведомить другие сервисы. - Inbox Pattern (Deduplication): Каждый консьюмер сначала записывает
message_idв таблицуinbox. Если придет дубликат сообщения из Kafka, вставка вinboxупадет по уникальному ключу, и сообщение будет проигнорировано. - Атомарные списания: В
PaymentsServiceиспользуется сырой SQLUPDATE accounts SET balance = balance - @amount WHERE user_id = @userId AND balance >= @amount RETURNING. Это предотвращает "овердрафт" (уход в минус) даже при параллельных запросах. - Идемпотентность БД: Таблица
paymentsимеет уникальный ключ наorder_id. Это гарантирует, что даже при сбое логики мы никогда не спишем деньги за один и тот же заказ дважды. - Manual Ack: Мы подтверждаем прочтение сообщения в Kafka (
Commit()) только после того, как транзакция в базе успешно завершена.
dotnet test
Покрывают маппинг статусов в Orders и формирование ответных сообщений Payments (reason/status), что является ключевой частью идемпотентности.