Система интернет-магазина "Gozon", построенная на микросервисной архитектуре с асинхронным межсервисным взаимодействием через RabbitMQ. Реализует паттерны Transactional Outbox и Transactional Inbox для гарантированной доставки сообщений и exactly-once семантики для финансовых операций.
Система состоит из следующих компонентов:
- API Gateway (
ApiGateway) — единая точка входа для всех клиентских запросов, маршрутизация на соответствующие сервисы - Orders Service (
OrdersService) — управление заказами: создание, просмотр списка, получение статуса - Payments Service (
PaymentsService) — управление платежами: создание счёта, пополнение баланса, просмотр баланса
- RabbitMQ — брокер сообщений для асинхронной коммуникации между сервисами
- PostgreSQL — реляционная БД (отдельная база для каждого микросервиса)
- Frontend — веб-интерфейс на HTML/CSS/JavaScript
Frontend → API Gateway → Orders Service
↓ (async)
RabbitMQ (payment.requests)
↓
Payments Service
↓ (async)
RabbitMQ (payment.results)
↓
Orders Service (обновление статуса)
- Docker и Docker Compose
- .NET 9.0 SDK
docker compose up --buildСистема автоматически:
- Создаст и инициализирует все базы данных
- Настроит очереди RabbitMQ
- Запустит все микросервисы с правильными зависимостями
После запуска доступны:
- Frontend: http://localhost:3000
- API Gateway: http://localhost:8080
- Orders Service Swagger: http://localhost:8081/swagger
- Payments Service Swagger: http://localhost:8082/swagger
- RabbitMQ Management UI: http://localhost:15672 (логин:
guest, пароль:guest)
docker compose downДля полной очистки данных:
docker compose down -vВсе запросы к API должны содержать заголовок X-User-Id с идентификатором пользователя (строка).
POST /api/payments/account
X-User-Id: user-1Ответ:
{
"balanceRub": 0.00
}POST /api/payments/account/topup
X-User-Id: user-1
Content-Type: application/json
{
"amountRub": 1000.00
}Ответ:
{
"balanceRub": 1000.00
}GET /api/payments/account/balance
X-User-Id: user-1Ответ:
{
"balanceRub": 1000.00
}POST /api/orders
X-User-Id: user-1
Content-Type: application/json
{
"amountRub": 250.00,
"description": "Заказ товаров"
}Ответ:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"amountRub": 250.00,
"description": "Заказ товаров",
"status": "NEW",
"createdAt": "2025-12-15T10:30:00Z"
}Примечание: Создание заказа асинхронно запускает процесс оплаты через RabbitMQ. Статус заказа обновится после обработки платежа.
GET /api/orders
X-User-Id: user-1Ответ:
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"amountRub": 250.00,
"description": "Заказ товаров",
"status": "FINISHED",
"createdAt": "2025-12-15T10:30:00Z"
}
]GET /api/orders/{orderId}
X-User-Id: user-1Ответ:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"amountRub": 250.00,
"description": "Заказ товаров",
"status": "FINISHED",
"createdAt": "2025-12-15T10:30:00Z"
}NEW— заказ создан, ожидает оплатыFINISHED— оплата успешно выполненаCANCELLED— оплата отклонена (недостаточно средств или другая ошибка)
RabbitMQ обеспечивает доставку сообщений минимум один раз. Consumer подтверждает сообщение (ack) только после успешной обработки в БД. При ошибке выполняется nack с requeue=true для повторной обработки.
При создании заказа в одной транзакции:
- Запись заказа в таблицу
orders - Запись сообщения в таблицу
outbox_messages
Фоновый воркер (OrdersOutboxPublisherHostedService):
- Периодически опрашивает
outbox_messagesна наличие неопубликованных сообщений - Публикует их в RabbitMQ (
payment.requests) - Помечает как опубликованные (
published_at) только после подтверждения от RabbitMQ (publisher-confirm)
Это гарантирует, что сообщение будет доставлено даже при сбое после коммита транзакции.
При получении сообщения из RabbitMQ:
- Проверяется наличие
message_idв таблицеinbox_messages - Если сообщение уже обработано — пропускается (no-op)
- Если новое — записывается в
inbox_messagesи обрабатывается
Это обеспечивает идемпотентность обработки дубликатов сообщений.
Для гарантии exactly-once списания используется комбинация:
- Таблица
processed_orders— хранит уникальныеorder_idобработанных заказов - Атомарное обновление баланса:
UPDATE accounts SET balance_cents = balance_cents - @AmountCents WHERE user_id = @UserId AND balance_cents >= @AmountCents
- Проверка уникальности: попытка вставить
order_idвprocessed_ordersс проверкой конфликта
Если заказ уже обработан, повторная обработка не приводит к повторному списанию.
Результат оплаты записывается в outbox_messages и публикуется воркером в очередь payment.results.
- At-least-once доставка: каждое сообщение доставлено минимум один раз
- Exactly-once обработка: каждое сообщение обработано ровно один раз (через Inbox)
- Exactly-once списание: каждый заказ списан ровно один раз (через
processed_orders+ атомарное обновление)
OnlineStore/
├── src/
│ ├── ApiGateway/ # API Gateway сервис
│ ├── OrdersService/ # Сервис заказов
│ │ ├── Background/ # Фоновые задачи (Outbox, Consumer)
│ │ ├── Infrastructure/ # БД, RabbitMQ подключения
│ │ └── OpenApi/ # Swagger конфигурация
│ ├── PaymentsService/ # Сервис платежей
│ │ ├── Background/ # Фоновые задачи (Inbox, Outbox, Consumer)
│ │ ├── Infrastructure/ # БД, RabbitMQ подключения
│ │ └── OpenApi/ # Swagger конфигурация
│ └── Shared/ # Общие контракты и утилиты
│ ├── Messaging/ # Контракты сообщений
│ └── Money.cs # Конвертация денег
├── frontend/ # Веб-интерфейс
│ ├── index.html
│ ├── app.js
│ ├── nginx.conf
│ └── Dockerfile
├── docs/
├── postman/
├── docker-compose.yml
└── README.md
- API: суммы в рублях (
decimal) с точностью до 2 знаков после запятой - Хранение: суммы в копейках (
long) для предотвращения ошибок округления - Конвертация: утилиты
Money.RubToCents()иMoney.CentsToRub()в проектеShared
- Orders DB: таблицы
orders,outbox_messages - Payments DB: таблицы
accounts,inbox_messages,processed_orders,payment_results,outbox_messages
Все таблицы создаются автоматически при первом запуске через HostedService.
{
"messageId": "uuid",
"messageType": "PaymentRequested",
"createdAt": "2025-12-15T10:30:00Z",
"correlationId": "order-id",
"payload": {
"orderId": "uuid",
"userId": "user-1",
"amountCents": 25000
}
}payment.requests— запросы на оплату (Orders → Payments)payment.results— результаты оплаты (Payments → Orders)
Все запросы требуют заголовок X-User-Id (строка). Сервисы извлекают его из HTTP заголовков и используют для фильтрации данных.
Готовая коллекция для тестирования API находится в postman/OnlineStore.postman_collection.json.
