Заказ в маркетплейсе — это не просто запись в базе. За ним стоит цепочка: выбрать товары, зарезервировать остатки, получить оплату, передать продавцу, доставить, и только потом закрыть. А если что-то пошло не так — спор или возврат. Весь этот процесс обслуживает один сервис — Order Service.
Разберём, как он устроен изнутри: какие сущности выделяются, как они общаются и почему спор — это отдельный объект, а не просто флаг у заказа.
Что делает Order Service
Его задача — провести заказ от первого «добавить в корзину» до финального «завершён». При этом сам сервис не хранит каталог товаров, не списывает деньги и не двигает остатки — это делают соседние сервисы. Order Service координирует их и хранит состояние заказа.
С ним работают три категории пользователей:
- Покупатель — создаёт заказ, оплачивает, может отменить или открыть спор.
- Продавец — видит заказы по своим товарам, отмечает отправку.
- Оператор — разбирает споры, может отменить любой заказ.
Жизненный цикл заказа
Заказ проходит через несколько статусов. Это не произвольные пометки, а строгая машина состояний — перейти из любого статуса в любой нельзя.
Заказ не висит в ожидании оплаты вечно: через пятнадцать минут он уходит в EXPIRED, и резерв на складе возвращается. Без этого состояния товар остался бы занят навсегда.
Дополнительные пути:
- Из
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.
У спора два исхода, и оба — законные концы: решение в пользу покупателя и в пользу продавца. Отдельного «отклонён» нет, иначе в отчётах появился бы третий, пустой случай.
Продавец получает 3 дня на ответ. Если промолчал — спор переходит в UNDER_REVIEW автоматически. Оператор изучает и выносит решение.
Важное правило: спор ≠ возврат. Если оператор решил в пользу продавца — никакого возврата нет, заказ завершается.
Агрегат Refund (возврат)
Возврат денег — тоже процесс, а не мгновенная операция. Нужно сначала снять резерв в Inventory, потом вернуть деньги через Payment. Каждый шаг может упасть, нужны повторы и компенсации.
Возврат денег может не пройти, и это не ошибка программы: у провайдера свои причины. Поэтому FAILED — обычное состояние, с которого начинается разбор, а не исключение.
Refund создаётся в двух случаях: покупатель отменил оплаченный заказ, или спор решился в его пользу. В обоих случаях Order ждёт события RefundCompleted и только после него переходит в REFUNDED.
Как агрегаты координируются
Три агрегата — три границы транзакций. Они не вызывают методы друг друга напрямую, а общаются через доменные события.
Пример: оплата заказа.
Никто никого не зовёт напрямую: каждый сервис слышит событие и делает своё. Заказ переходит в PAID не потому, что его попросили, а потому что пришло подтверждение оплаты.
Пример: спор с возвратом.
Каждый переход здесь — следствие события, а не прямого вызова: заказ не просит возврат «сделать деньги обратно», он лишь слышит, что возврат прошёл. Цена — состояние меняется не сразу, зато возврат можно переписать целиком, не трогая заказ.
Каждый переход — результат события. Агрегаты не знают деталей реализации друг друга, только форму события.
Как события не теряются: 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 гарантирует: изменение состояния и публикация события атомарны.
- Цена фиксируется при создании заказа и не меняется.
- Спор ≠ возврат: спор может решиться в пользу продавца без выплаты покупателю.
Что почитать дальше
- Как устроена Use Case спецификация — формат, из которого строится Order Service.
- DDD: агрегаты и доменные события — теория за трёхагрегатной моделью.
- Outbox и Saga в распределённых системах — как события не теряются при сбоях.