← назад к разделу

Заказ в маркетплейсе — это не просто запись в базе. За ним стоит цепочка: выбрать товары, зарезервировать остатки, получить оплату, передать продавцу, доставить, и только потом закрыть. А если что-то пошло не так — спор или возврат. Весь этот процесс обслуживает один сервис — Order Service.

Разберём, как он устроен изнутри: какие сущности выделяются, как они общаются и почему спор — это отдельный объект, а не просто флаг у заказа.

Что делает Order Service

Его задача — провести заказ от первого «добавить в корзину» до финального «завершён». При этом сам сервис не хранит каталог товаров, не списывает деньги и не двигает остатки — это делают соседние сервисы. Order Service координирует их и хранит состояние заказа.

С ним работают три категории пользователей:

  • Покупатель — создаёт заказ, оплачивает, может отменить или открыть спор.
  • Продавец — видит заказы по своим товарам, отмечает отправку.
  • Оператор — разбирает споры, может отменить любой заказ.

Жизненный цикл заказа

Заказ проходит через несколько статусов. Это не произвольные пометки, а строгая машина состояний — перейти из любого статуса в любой нельзя.

DRAFT → PENDING_PAYMENT → PAID → SHIPPED → DELIVERED → COMPLETED
                 ↓
              EXPIRED (не оплатили за 15 минут)

Дополнительные пути:

  • Из PAID или DRAFT — отмена в CANCELLED / REFUNDED.
  • Из DELIVERED — спор в DISPUTED, потом COMPLETED или REFUNDED.

DRAFT — покупатель набрал товары, заказ ещё не подтверждён. Можно менять позиции.

PENDING_PAYMENT — заказ подтверждён, остатки зарезервированы в Inventory. У покупателя есть 15 минут на оплату. Если не успел — заказ переходит в EXPIRED.

PAID — оплата прошла. Теперь шар на стороне продавца.

SHIPPED → DELIVERED — продавец отметил отправку, затем логистика подтверждает вручение.

COMPLETED — заказ закрыт. Происходит автоматически через 14 дней после доставки, если покупатель не открыл спор.

Почему спор и возврат — отдельные сущности

Первое желание — добавить поле disputeReason в таблицу orders. Но тогда у спора нет собственного жизненного цикла: нельзя отслеживать, ответил ли продавец, когда истекает срок, что решил оператор.

В Order Service спор и возврат выделены в отдельные агрегаты — самостоятельные объекты с собственными состояниями.

Агрегат Dispute (спор)

Покупатель может открыть спор в течение 14 дней после доставки. Спор живёт отдельно, но ссылается на заказ по ID.

OPEN → AWAITING_SELLER → UNDER_REVIEW → RESOLVED_FOR_BUYER
                                      ↘ RESOLVED_FOR_SELLER

Продавец получает 3 дня на ответ. Если промолчал — спор переходит в UNDER_REVIEW автоматически. Оператор изучает и выносит решение.

Важное правило: спор ≠ возврат. Если оператор решил в пользу продавца — никакого возврата нет, заказ завершается.

Агрегат Refund (возврат)

Возврат денег — тоже процесс, а не мгновенная операция. Нужно сначала снять резерв в Inventory, потом вернуть деньги через Payment. Каждый шаг может упасть, нужны повторы и компенсации.

REQUESTED → RESERVATION_RELEASED → REFUNDING → COMPLETED
         ↘                                   ↘ FAILED

Refund создаётся в двух случаях: покупатель отменил оплаченный заказ, или спор решился в его пользу. В обоих случаях Order ждёт события RefundCompleted и только после него переходит в REFUNDED.

Как агрегаты координируются

Три агрегата — три границы транзакций. Они не вызывают методы друг друга напрямую, а общаются через доменные события.

Пример: оплата заказа.

Покупатель → подтверждает заказ
Order: DRAFT → PENDING_PAYMENT, публикует OrderConfirmed
Inventory: получает OrderConfirmed → резервирует остатки → публикует ItemReserved
Покупатель → платит
Payment → публикует PaymentSucceeded
Order: получает PaymentSucceeded → PENDING_PAYMENT → PAID

Пример: спор с возвратом.

Покупатель открывает спор → Dispute создаётся
Order: DELIVERED → DISPUTED
...оператор решает в пользу покупателя...
Dispute публикует DisputeResolved(buyer)
Order создаёт Refund
Refund идёт в Inventory (снять резерв) и Payment (вернуть деньги)
Refund публикует RefundCompleted
Order: DISPUTED → REFUNDED

Каждый переход — результат события. Агрегаты не знают деталей реализации друг друга, только форму события.

Как события не теряются: Outbox

Представьте: Order записал PAID в базу, а потом упал до отправки события в Kafka. Inventory так и не узнал о заказе. Деньги списаны, резерва нет.

Для этого используют паттерн Outbox: событие записывается в ту же транзакцию, что и изменение состояния агрегата. Отдельный процесс (relay) читает таблицу outbox и отправляет события в Kafka. Если relay упал — перезапустится и отправит снова. Получатели обрабатывают события идемпотентно (запоминают уже обработанные).

Что заказ знает о ценах

Цены фиксируются в момент оформления заказа. Если продавец поднял цену через час — ваш заказ уже содержит старую цену в поле unitPrice каждой позиции. Это правило BR-O04: цена позиции в заказе никогда не меняется после создания.

Что хранится в базе

Основные таблицы:

  • orders — заголовок заказа: статус, итоговая сумма, ссылки на резерв и платёж.
  • order_items — позиции: товар, продавец, количество, цена на момент покупки.
  • disputes — споры, логическая ссылка на заказ по order_id (без внешнего ключа — разные агрегаты).
  • refunds — возвраты, аналогично.
  • outbox — события, ждущие отправки в Kafka.

Связи между агрегатами — только через order_id без FK. Это принципиально: каждый агрегат можно масштабировать, мигрировать и деплоить независимо.

Коротко

  • Order Service координирует заказ, но не хранит каталог, остатки и деньги — это соседние сервисы.
  • Заказ проходит строгую машину состояний: DRAFT → PENDING_PAYMENT → PAID → SHIPPED → DELIVERED → COMPLETED.
  • Спор и возврат — отдельные агрегаты со своим жизненным циклом, потому что у них своя логика, таймауты и правила.
  • Агрегаты координируются через доменные события — никаких прямых вызовов между ними.
  • Outbox гарантирует: изменение состояния и публикация события атомарны.
  • Цена фиксируется при создании заказа и не меняется.
  • Спор ≠ возврат: спор может решиться в пользу продавца без выплаты покупателю.

Что почитать дальше