Флагман кейса: контекст Order на Уровне 3 (DDD + Hexagonal) с тремя агрегатамиOrder (оформление), Dispute (спор после доставки), Refund (процесс возврата). Спека показывает мультиагрегатный формат: контекст-секции (язык, роли, события-контракт, интеграции) — общие; модель, жизненный цикл, команды и события — по агрегату. В репозитории это корневой файл + aggregates/{order,dispute,refund}.md; здесь — одной страницей.

Принцип: домен без техники. Вся реализация (фреймворки, схема БД, Outbox, топики, стек) — в одном разделе «Техническая реализация»; остальное — в доменных терминах.


1. Bounded Context

Контекст: Order. Субдомен: Ordering — Core (оформление заказа — конкурентное ядро маркетплейса). Владелец: команда order-team. Миссия: довести заказ от черновика до закрытия, удерживая согласованность денег, резерва и состояния.

АгрегатРоль
Orderкорень оформления: позиции, сумма, статусы, координация резерва/оплаты/доставки
Disputeспор после доставки: ответ продавца, решение оператора
Refundпроцесс возврата денег: снятие резерва → возврат средств

Агрегаты — отдельные границы транзакций; ссылаются друг на друга только по ID (orderId) и координируются доменными событиями.

Внутри границы: жизненный цикл заказа, спор и возврат на стороне заказа, публикация доменных событий. Вне границы: каталог и цены (Catalog), движение остатка (Inventory), платёжные шлюзы (Payment), расчёты с продавцами (Settlement), аутентификация (Keycloak).


2. Интеграции (Context Map)

diagram
РеброНаправлениеКаналСвязьПередаётся
Customer/Seller/Admin BFFinboundsynccustomer-supplierкоманды покупателя/продавца/оператора
Catalogoutboundsyncconformistцены, наличие товара
Inventorybidirectionalasynccustomer-supplierpublish OrderConfirmed/OrderCancelled/OrderExpired; consume ItemReserved/ReservationFailed/ReservationReleased
Paymentbidirectionalsync+asynccustomer-supplierзапрос оплаты/возврата; consume PaymentSucceeded/PaymentFailed/RefundIssued
Notificationoutboundasyncohsсобытия заказа и спора
SettlementoutboundasyncohsOrderPaid/OrderCompleted/OrderRefunded

Контракты: REST → OpenAPI, async → AsyncAPI; владелец по типу связи (для conformist и потребляемых событий — upstream; для своих REST и публикуемых событий — Order). Файлы: contracts/order-api.openapi.yaml, contracts/order-events.asyncapi.yaml.


3. Ubiquitous Language

ТерминВ кодеОпределение
ЗаказOrderФинансово-обязывающий документ. Корень агрегата.
ПозицияOrderItemТовар × количество × цена на момент покупки.
СуммаMoney totalПозиции − скидка + доставка. Вычисляется.
РезервReservationБлокировка остатка в Inventory до оплаты.
СпорDisputeОтдельный агрегат: разбирательство после доставки.
ВозвратRefundОтдельный агрегат: процесс возврата денег.
Доменное событиеDomainEventФакт в агрегате; публикуется наружу.

Не путать: Корзина ≠ Заказ · Платёж ≠ Заказ · Отмена ≠ Возврат · Резерв ≠ Списание · Спор ≠ Возврат (спор может закончиться в пользу продавца — без возврата).


4. Роли и доступ

РольКто
customer / seller / adminпокупатель / продавец / оператор
systemвнутренние сервисы (Payment, Inventory, логистика)

ABAC (общие): владение покупателя ⇔ order.customerId == jwt.sub; доступ продавца ⇔ ∃ OrderItem.sellerId == jwt.sub; admin — полный доступ. Доступ к операциям — в матрице каждого агрегата ниже. PII: Address — шифрование хранения; email/phone не хранятся.


5. Доменные события (контракт публикуемого языка)

События владеются агрегатами (полное описание — в разделе каждого агрегата). Наружу публикуются только внешние:

АгрегатВнешние событияТопик
OrderOrderConfirmed, OrderReservationFailed, OrderPaid, OrderPaymentFailed, OrderShipped, OrderDelivered, OrderCompleted, OrderCancelled, OrderExpired, OrderRefundedmarketplace.orders.v1
DisputeDisputeOpened, DisputeResolvedmarketplace.disputes.v1
RefundRefundCompletedmarketplace.refunds.v1

6. Use Cases

UC-1 Покупка (happy path). Order: CreateOrderConfirmOrder → резерв (Inventory) → оплата (Payment) → PAIDMarkShippedMarkDelivered → через 14 дней COMPLETED. Альт: неудача резерва/оплаты → DRAFT; уход с оплаты → EXPIRED.

UC-2 Отмена до отправки. Order.CancelOrder оплаченного → создаётся RefundRefundCompletedREFUNDED.

UC-3 Спор → возврат. DELIVEREDDispute.OpenDispute (OrderDISPUTED) → продавец отвечает или таймаут 3 дня → оператор ResolveDispute. В пользу покупателя → RefundREFUNDED; в пользу продавца → COMPLETED.

UC-4 Отправка продавцом. PAIDMarkShippedSHIPPED.

UC-5/6 Поиск. SearchMyOrders / SearchSellerOrders. UC-7 Разбор споров оператором: ListOpenDisputesResolveDispute.


7. Процессы (Saga / Process Manager)

Подтверждение (внутри Order, координирует Inventory + Payment):

diagram

Спор → Возврат (DisputeOrderRefund):

diagram

8. UI-спецификация

UI у сервиса нет — экраны в Customer/Seller/Admin BFF; здесь связь статусов с UI.

СтатусБейджЦвет
DRAFT/PENDING_PAYMENT/PAID/SHIPPED/DELIVEREDЧерновик/Ожидает оплаты/Оплачен/Отправлен/Доставленсерый/жёлтый/синий/фиолетовый/зелёный
COMPLETED/EXPIRED/CANCELLED/DISPUTED/REFUNDEDЗавершён/Истёк/Отменён/Спор/Возвраттёмно-зелёный/тёмно-серый/тёмно-серый/красный/серо-синий

Тексты ошибок для пользователя — по кодам из разделов «Команды» агрегатов (OUT_OF_STOCK → «Один из товаров закончился», ORDER_BELOW_MINIMUM → «Минимальная сумма заказа — 100 ₽» и т.д.).


9. Критерии приёмки (Given / When / Then)

  • Given корзина одного продавца · When оформление + подтверждение + оплата · Then DRAFT → PENDING_PAYMENT → PAID, OrderPaid.
  • Given тот же идемпотентный ключ · When повторное создание · Then прежний orderId (BR-O07).
  • Given оплаченный заказ · When отмена · Then Refund; OrderRefunded только после RefundCompleted.
  • Given доставлен в окне 14 дней · When открытие спора · Then Dispute OPEN, Order DISPUTED.
  • Given спор UNDER_REVIEW · When решение в пользу покупателя/продавца · Then REFUNDED/COMPLETED.
  • Given заказ без товара продавца · When MarkShipped · Then FORBIDDEN (BR-O05).

10. Нефункциональные требования

АспектТребование
ПроизводительностьCreateOrder p95 < 1.5s; ConfirmOrder p95 < 800ms; GetOrderById p95 < 100ms; до 500 RPS в пик
ДоступностьSLO 99.9%; деградация Catalog/Payment/Inventory не роняет контекст (изоляция отказов, таймауты)
Согласованностьвнутри агрегата — строгая; между агрегатами/контекстами — eventual ≤5с; чтение — eventual + RYOW для critical-path
БезопасностьTLS + mTLS; роли + ABAC; аудит оператора (5 лет); Address шифруется; 152-ФЗ
Наблюдаемостьметрики операций/состояний; трассировка по orderId; алёрты на отставание событий и circuit breaker

11. Техническая реализация

java-21 · spring-boot-3 · usecase-pattern-starter + ddd-building-blocks (ru.vikulinva) · postgresql-16 + jooq + flyway · kafka + spring-kafka + Outbox-relay · redis · mapstruct · resilience4j · Spring Security + Keycloak + mTLS · Micrometer/Prometheus + OpenTelemetry/Tempo + Loki/Grafana · Testcontainers + WireMock + Gatling.

Контейнеры (C2): Order App · PostgreSQL · Kafka · Redis. Outbox — события публикуются атомарно с изменением агрегата (@TransactionalEventListener обновляет Read Model). Топики: marketplace.orders.v1 / .disputes.v1 / .refunds.v1; идемпотентный приём (processed_events).

Схема БД

Внутри агрегата — FK; между агрегатами связь по ID, без FK (disputes.order_id, refunds.order_id — логические ссылки на orders.id).

diagram

Read Model: order_summaries, order_timelines, dispute_queue — проекции из событий. Ошибки — RFC 9457 ProblemDetails; HTTP-статусы из кодов в «Командах» агрегатов.


Агрегат Order

Корень оформления. Координирует резерв, оплату и доставку; делегирует спор Dispute, возврат — Refund (по orderId).

Доменная модель

ЭлементТипРоль
OrderAggregate Rootпозиции, статус, сумма, ссылки на резерв и платёж
OrderItemEntityпозиция; уникальна по (productId, sellerId); не существует вне Order

Value Objects (через инвариант): Money (сумма+валюта RUB, immutable, не отрицательна) · Quantity (1..999) · Discount (sealed: Percentage | Fixed) · OrderStatus · Address (PII) · типизированные ID.

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

СтатусОписание
DRAFTможно менять позиции
PENDING_PAYMENTрезерв успешен, ждём оплату (15 мин)
PAID / SHIPPED / DELIVEREDоплачен / отправлен / вручён (окно спора 14 дней)
DISPUTEDактивен спор (агрегат Dispute)
COMPLETED / EXPIRED / CANCELLED / REFUNDEDтерминальные (кроме CANCELLED)
diagram

Доступ

ОперацияcustomerselleradminsystemABAC
CreateOrder/AddItem/ApplyPromo/ConfirmOrder/CancelOrderвладение
MarkShippedзаказ содержит товар продавца
MarkDeliveredsystem = логистика
GetOrderById/SearchMyOrdersсвой / содержит товар / admin

Бизнес-правила

BR-O01 сумма total = Σ lineTotal − discount + shippingFee · BR-O02 резерв обязателен перед PENDING_PAYMENT (→ OUT_OF_STOCK) · BR-O03 один промокод · BR-O04 unitPrice фиксируется при оформлении · BR-O05 MarkShipped только продавцу позиции (→ FORBIDDEN) · BR-O06 отмена оплаченного — через Refund · BR-O07 идемпотентный CreateOrder · BR-O08 PaymentSucceeded ровно один раз · BR-O10 total ≥ 100 RUB (→ ORDER_BELOW_MINIMUM) · BR-O11 один заказ → один продавец (V1) · BR-O12 события атомарны с изменением состояния.

Команды

КомандаПереходЭмититОшибки
CreateOrder∅ → DRAFTOrderCreatedPRODUCT_NOT_FOUND, MULTI_SELLER_NOT_SUPPORTED
ConfirmOrderDRAFTPENDING_PAYMENTOrderConfirmedORDER_INVALID_STATE, ORDER_BELOW_MINIMUM, EMPTY_ORDER
MarkShippedPAIDSHIPPEDOrderShippedORDER_INVALID_STATE, FORBIDDEN
MarkDeliveredSHIPPEDDELIVEREDOrderDeliveredORDER_INVALID_STATE
CancelOrderдо отправки → CANCELLED; оплаченного → REFUNDEDOrderCancelledORDER_INVALID_STATE

Реакции (запускаются событиями/таймером, не актором) — в матрице переходов: PaymentSucceededPAID/OrderPaid; PaymentFailedDRAFT/OrderPaymentFailed; DisputeOpenedDISPUTED; DisputeResolved(seller)COMPLETED; RefundCompletedREFUNDED; таймаут 15 мин → EXPIRED; окно 14 дней → COMPLETED.

Доменные события

СобытиеТриггерScopeПодписчики
OrderCreatedCreateOrderвнутреннееRead Model
OrderConfirmedConfirmOrderвнешнееInventory, Notification
OrderPaidуспешный платёжвнешнееNotification, Inventory, Settlement
OrderShipped/OrderDeliveredMarkShipped/MarkDeliveredвнешнееNotification
OrderCompletedзакрытие / спор в пользу продавцавнешнееSettlement
OrderCancelled/OrderExpiredCancelOrder / истечениевнешнееInventory, Notification
OrderRefundedреакция на RefundCompletedвнешнееSettlement, Notification

Запросы

GetOrderById (write-side, RYOW) · SearchMyOrders (Read Model OrderSummary, фильтр по customerId) · SearchSellerOrders (по позициям продавца) · GetOrderTimeline (OrderTimeline) — все согласованы в конечном счёте, кроме GetOrderById.


Агрегат Dispute

Спор по доставленному заказу. Своя жизнь (ответ продавца в срок, вложения, вердикт) — раньше раздувал Order. Ссылается на заказ по orderId.

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

diagram

Доступ и правила

Доступ: OpenDispute — customer/admin (владение заказом); SubmitSellerResponse — seller; ResolveDispute — admin. Правила: BR-D01 спор только покупателем в окне 14 дней после DELIVERED (→ DISPUTE_WINDOW_CLOSED) · BR-D02 один активный спор на заказ (→ DISPUTE_ALREADY_OPEN) · BR-D03 3 дня продавцу на ответ, иначе авто-UNDER_REVIEW · BR-D04 решает только admin · BR-D05 решение в пользу покупателя инициирует ровно один Refund.

Команды и события

КомандаПереходЭмитит
OpenDispute∅ → OPENDisputeOpened
SubmitSellerResponseAWAITING_SELLERUNDER_REVIEWSellerResponded
ResolveDisputeUNDER_REVIEWRESOLVED_FOR_BUYER/SELLERDisputeResolved

DisputeOpened / DisputeResolved — внешние (Notification, Order, Refund если в пользу покупателя); SellerResponded — внутреннее. Запросы: GetDispute, ListOpenDisputes (очередь оператора из DisputeQueue), GetDisputesByOrder.


Агрегат Refund

Процесс возврата денег: снятие резерва в Inventory → возврат средств через Payment. Агрегат-процесс со своим жизненным циклом, компенсациями и таймаутами.

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

diagram

Правила, команда, события

BR-R01 возврат только для оплаченного заказа · BR-R02 если выплата продавцу исполнена — баланс продавца уходит в минус · BR-R03 шаги идемпотентны; неподтверждение за 5 мин — повтор, после 3 неудач → FAILED + эскалация · BR-R04 один незавершённый Refund на заказ.

Команда RequestRefund (admin/system, ∅ → REQUESTED, эмитит RefundRequested) инициируется из Order при CancelOrder оплаченного и при DisputeResolved(buyer). Реакции: ReservationReleasedREFUNDING; RefundIssuedCOMPLETED + RefundCompleted; RefundFailedFAILED. По RefundCompleted Order переходит в REFUNDED. Запросы: GetRefund, GetRefundByOrder.


Машинно-читаемая копия — docs/spec/order-service-spec.md + aggregates/{order,dispute,refund}.md в репозитории (github.com/remodov/order-service): корень контекста + по файлу на агрегат, из которых скиллы методологии генерируют код, диаграммы (Structurizr) и проверяют дрейф.