Флагман кейса: контекст 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)
| Ребро | Направление | Канал | Связь | Передаётся |
|---|---|---|---|---|
| Customer/Seller/Admin BFF | inbound | sync | customer-supplier | команды покупателя/продавца/оператора |
| Catalog | outbound | sync | conformist | цены, наличие товара |
| Inventory | bidirectional | async | customer-supplier | publish OrderConfirmed/OrderCancelled/OrderExpired; consume ItemReserved/ReservationFailed/ReservationReleased |
| Payment | bidirectional | sync+async | customer-supplier | запрос оплаты/возврата; consume PaymentSucceeded/PaymentFailed/RefundIssued |
| Notification | outbound | async | ohs | события заказа и спора |
| Settlement | outbound | async | ohs | OrderPaid/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. Доменные события (контракт публикуемого языка)
События владеются агрегатами (полное описание — в разделе каждого агрегата). Наружу публикуются только внешние:
| Агрегат | Внешние события | Топик |
|---|---|---|
Order | OrderConfirmed, OrderReservationFailed, OrderPaid, OrderPaymentFailed, OrderShipped, OrderDelivered, OrderCompleted, OrderCancelled, OrderExpired, OrderRefunded | marketplace.orders.v1 |
Dispute | DisputeOpened, DisputeResolved | marketplace.disputes.v1 |
Refund | RefundCompleted | marketplace.refunds.v1 |
6. Use Cases
UC-1 Покупка (happy path). Order: CreateOrder → ConfirmOrder → резерв (Inventory) → оплата (Payment) → PAID → MarkShipped → MarkDelivered → через 14 дней COMPLETED. Альт: неудача резерва/оплаты → DRAFT; уход с оплаты → EXPIRED.
UC-2 Отмена до отправки. Order.CancelOrder оплаченного → создаётся Refund → RefundCompleted → REFUNDED.
UC-3 Спор → возврат. DELIVERED → Dispute.OpenDispute (Order → DISPUTED) → продавец отвечает или таймаут 3 дня → оператор ResolveDispute. В пользу покупателя → Refund → REFUNDED; в пользу продавца → COMPLETED.
UC-4 Отправка продавцом. PAID → MarkShipped → SHIPPED.
UC-5/6 Поиск. SearchMyOrders / SearchSellerOrders. UC-7 Разбор споров оператором: ListOpenDisputes → ResolveDispute.
7. Процессы (Saga / Process Manager)
Подтверждение (внутри Order, координирует Inventory + Payment):
Спор → Возврат (Dispute → Order → Refund):
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
DisputeOPEN,OrderDISPUTED. - Given спор
UNDER_REVIEW· When решение в пользу покупателя/продавца · ThenREFUNDED/COMPLETED. - Given заказ без товара продавца · When
MarkShipped· ThenFORBIDDEN(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).
Read Model: order_summaries, order_timelines, dispute_queue — проекции из событий. Ошибки — RFC 9457 ProblemDetails; HTTP-статусы из кодов в «Командах» агрегатов.
Агрегат Order
Корень оформления. Координирует резерв, оплату и доставку; делегирует спор Dispute, возврат — Refund (по orderId).
Доменная модель
| Элемент | Тип | Роль |
|---|---|---|
Order | Aggregate Root | позиции, статус, сумма, ссылки на резерв и платёж |
OrderItem | Entity | позиция; уникальна по (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) |
Доступ
| Операция | customer | seller | admin | system | ABAC |
|---|---|---|---|---|---|
CreateOrder/AddItem/ApplyPromo/ConfirmOrder/CancelOrder | ✅ | — | ✅ | — | владение |
MarkShipped | — | ✅ | ✅ | — | заказ содержит товар продавца |
MarkDelivered | — | — | ✅ | ✅ | system = логистика |
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 | ∅ → DRAFT | OrderCreated | PRODUCT_NOT_FOUND, MULTI_SELLER_NOT_SUPPORTED |
ConfirmOrder | DRAFT → PENDING_PAYMENT | OrderConfirmed | ORDER_INVALID_STATE, ORDER_BELOW_MINIMUM, EMPTY_ORDER |
MarkShipped | PAID → SHIPPED | OrderShipped | ORDER_INVALID_STATE, FORBIDDEN |
MarkDelivered | SHIPPED → DELIVERED | OrderDelivered | ORDER_INVALID_STATE |
CancelOrder | до отправки → CANCELLED; оплаченного → REFUNDED | OrderCancelled | ORDER_INVALID_STATE |
Реакции (запускаются событиями/таймером, не актором) — в матрице переходов: PaymentSucceeded → PAID/OrderPaid; PaymentFailed → DRAFT/OrderPaymentFailed; DisputeOpened → DISPUTED; DisputeResolved(seller) → COMPLETED; RefundCompleted → REFUNDED; таймаут 15 мин → EXPIRED; окно 14 дней → COMPLETED.
Доменные события
| Событие | Триггер | Scope | Подписчики |
|---|---|---|---|
OrderCreated | CreateOrder | внутреннее | Read Model |
OrderConfirmed | ConfirmOrder | внешнее | Inventory, Notification |
OrderPaid | успешный платёж | внешнее | Notification, Inventory, Settlement |
OrderShipped/OrderDelivered | MarkShipped/MarkDelivered | внешнее | Notification |
OrderCompleted | закрытие / спор в пользу продавца | внешнее | Settlement |
OrderCancelled/OrderExpired | CancelOrder / истечение | внешнее | Inventory, Notification |
OrderRefunded | реакция на RefundCompleted | внешнее | Settlement, Notification |
Запросы
GetOrderById (write-side, RYOW) · SearchMyOrders (Read Model OrderSummary, фильтр по customerId) · SearchSellerOrders (по позициям продавца) · GetOrderTimeline (OrderTimeline) — все согласованы в конечном счёте, кроме GetOrderById.
Агрегат Dispute
Спор по доставленному заказу. Своя жизнь (ответ продавца в срок, вложения, вердикт) — раньше раздувал Order. Ссылается на заказ по orderId.
Жизненный цикл
Доступ и правила
Доступ: 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 | ∅ → OPEN | DisputeOpened |
SubmitSellerResponse | AWAITING_SELLER → UNDER_REVIEW | SellerResponded |
ResolveDispute | UNDER_REVIEW → RESOLVED_FOR_BUYER/SELLER | DisputeResolved |
DisputeOpened / DisputeResolved — внешние (Notification, Order, Refund если в пользу покупателя); SellerResponded — внутреннее. Запросы: GetDispute, ListOpenDisputes (очередь оператора из DisputeQueue), GetDisputesByOrder.
Агрегат Refund
Процесс возврата денег: снятие резерва в Inventory → возврат средств через Payment. Агрегат-процесс со своим жизненным циклом, компенсациями и таймаутами.
Жизненный цикл
Правила, команда, события
BR-R01 возврат только для оплаченного заказа · BR-R02 если выплата продавцу исполнена — баланс продавца уходит в минус · BR-R03 шаги идемпотентны; неподтверждение за 5 мин — повтор, после 3 неудач → FAILED + эскалация · BR-R04 один незавершённый Refund на заказ.
Команда RequestRefund (admin/system, ∅ → REQUESTED, эмитит RefundRequested) инициируется из Order при CancelOrder оплаченного и при DisputeResolved(buyer). Реакции: ReservationReleased → REFUNDING; RefundIssued → COMPLETED + RefundCompleted; RefundFailed → FAILED. По RefundCompleted Order переходит в REFUNDED. Запросы: GetRefund, GetRefundByOrder.
Машинно-читаемая копия — docs/spec/order-service-spec.md + aggregates/{order,dispute,refund}.md в репозитории (github.com/remodov/order-service): корень контекста + по файлу на агрегат, из которых скиллы методологии генерируют код, диаграммы (Structurizr) и проверяют дрейф.