Тот момент, когда они становятся сложнее самих доменов
Я столкнулся с такой проблемой:
логика между доменами сложнее самих доменов
а в книжках об этом не пишут
Я строил систему по DDD, все красиво:
-
домены
-
агрегаты
-
use cases
-
события.
Потом пришёл сценарий: «Отменить заказ»
Я думал: «Ну, Order::cancel(), вызову inventory.release(), pricing.refund(), и готово»
Но, хмм…
Если доставка уже в пути — нужно создать возвратную накладную
Если платёж падал дважды — отменить всё, а при первой попытке — только заморозить баллы
Если товара нет — перенести резерв на другой склад, пересчитать доставку, спросить клиента, если дороже
Если клиент повторил платёж — восстановить резерв и доставку
И я понял:
Самая сложная логика — не в доменах, а между ними. В книжках по DDD, Clean Architecture, Hexagonal — об этом не пишут. Там учат:
-
«Use case должен быть тонким»
-
«Домен это центр ответственности»
-
«Между доменами только события или вызовы API c JSON (т.е. типы на границе теряются, значит бизнес правила границу не переходят)»
Если я пойду по книжкам и воткну этот функционал в обязанности Order, то тот станет «божественным объектом»
-
Зависеть от 4 доменов
-
Знать про статус доставки
-
Принимать решения по retry, hold, freeze
-
Нарушать SRP
-
А заодно придется добавлять anti corruption layer т.к. на вход может прийти что угодно, я лишаюсь гарантий компилятора, выраженных в типах
Но никто не говорит, что делать то?
Я ввёл то, чего не было в учебниках:
CrossDomainCoordinator
-
Принимает на себя зависимости и логику, которые лишние, чужеродные для бизнес доменов
-
Знает, что делать дальше
-
Вызывает домены параллельно
-
Управляет политиками: retry, hold, freeze
-
Публикует события для аудита
SharedKernel
-
Самая устойчивая логика для всех, как политика Компании
Вкратце:
if status == InTransit { self.delivery.cancel_with_fee(...).await?; self.delivery.create_return_label(...).await?; } let (inv, loy) = tokio::join!( self.inventory.cancel_reservation(...), self.loyalty.rollback_points(...), ); self.pricing.refund_payment(...).await?; self.event_bus.publish(...).await; Ok(()) }
src/
├── domain/
│ ├── pricing/
│ │ └── src/lib.rs
│ ├── inventory/
│ │ └── src/lib.rs
│ ├── delivery/
│ │ └── src/lib.rs
│ └── loyalty/
│ └── src/lib.rs
├── application/
│ ├── coordination/
│ │ ├── mod.rs
│ │ ├── handlers.rs
│ │ └── events.rs
│ └── use_cases/
│ ├── create_order.rs
│ ├── cancel_order.rs
│ ├── confirm_order_payment.rs
│ └── retry_payment.rs
├── infrastructure/
│ ├── web/
│ │ ├── handlers.rs
│ │ ├── routes.rs
│ │ └── state.rs
│ ├── adapters/
│ │ ├── mock_*.rs
│ │ └── in_memory_repository.rs
│ └── persistence/
│ ├── mod.rs
│ ├── order_repository.rs
│ └── schema.sql
└── shared/lib.rs
В коде видно, что:
-
домены простые, включают больше типов данных, чем поведения (логика заключена в типах), остались чистыми: inventory, delivery, loyalty — не знают друг о друге
-
координатор сложный и состоит почти полностью из инвариантов (правил) поведения, не имеет сущностей внутри, единственное место, где принимаются решения, зависящие от нескольких доменов
На самом деле, я схалявил и не написал для доменов логику, только заглушки. Ну, и так понятно что она простая.
Вывод
Если логика между доменами сложнее самих доменов — выдели её явно CrossDomainCoordinator — как домен, только без своих сущностей. Для такого поведенческого домена может быть отдельная команда разработки.
всего-то 700+ строк учебного проекта
//! # Shared Kernel //! //! Минимальное, стабильное ядро. //! //! ## Правила: //! - Только фундаментальные типы //! - Никаких enum с бизнес-семантикой (Carrier, RefundReason и т.д.) //! - Никаких изменяемых политик //! //! ## Почему: //! Это shared kernel по DDD. Любое изменение здесь затрагивает всю систему. //! Поэтому он должен быть как "стандартная библиотека" — почти не меняется. use rust_decimal::Decimal; use uuid::Uuid; // ————————————— // Идентификаторы // ————————————— pub type CustomerId = Uuid; pub type ProductId = Uuid; pub type OrderId = Uuid; pub type WarehouseId = Uuid; pub type ReservationId = Uuid; pub type DeliveryId = Uuid; pub type RefundId = Uuid; pub type FreezeId = Uuid; // ————————————— // Денежные и количественные типы // ————————————— pub type Money = Decimal; pub type Quantity = u32; pub type Weight = f32; // ————————————— // Общие структуры // ————————————— #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct Address { pub street: String, pub city: String, pub zip: String, pub country: String, } ``` --- ### `src/domain/pricing/src/lib.rs` ```rust //! # Pricing Domain //! //! Управляет расчётом цен, блокировкой и возвратом средств. //! //! ## Зависит только от `shared/` //! - `Money`, `OrderId`, `CustomerId` //! //! ## Не зависит от других доменов use crate::shared::{CustomerId, ProductId, OrderId, Money, Quantity}; use uuid::Uuid; #[derive(Debug, Clone)] pub struct PriceRequest { pub customer_id: CustomerId, pub items: Vec<(ProductId, Quantity)>, pub promo_code: Option<String>, pub loyalty_discount: Option<Money>, } #[derive(Debug, Clone, serde::Serialize)] pub struct PriceBreakdown { pub base: Money, pub discount: Money, pub tax: Money, pub final_price: Money, } #[derive(Debug, Clone)] pub struct RefundRequest { pub order_id: OrderId, pub amount: Money, pub reason: RefundReason, } #[derive(Debug, Clone, PartialEq)] pub enum RefundReason { CustomerCancelled, Fraudulent, SystemError, } pub type PaymentHoldId = Uuid; // ————————————— // Функции // ————————————— pub fn calculate_prices(request: PriceRequest) -> PriceBreakdown { // Упрощённый расчёт let base: Money = request.items.iter().map(|(_, q)| Money::from(*q)).sum(); let discount = request.loyalty_discount.unwrap_or(Money::zero()); let tax = base * rust_decimal_macros::dec!(0.1); let final_price = base - discount + tax; PriceBreakdown { base, discount, tax, final_price, } } pub fn refund_payment(request: RefundRequest) -> Result<RefundId, PricingError> { // Интеграция с платёжной системой Ok(Uuid::new_v4()) } pub fn hold_payment(customer_id: CustomerId, amount: Money) -> Result<PaymentHoldId, PricingError> { Ok(Uuid::new_v4()) } // ————————————— // Ошибки // ————————————— #[derive(Debug, thiserror::Error)] pub enum PricingError { #[error("Ошибка платежной системы")] PaymentSystemError, #[error("Сумма отрицательная")] NegativeAmount, } ``` --- ### `src/domain/inventory/src/lib.rs` ```rust //! # Inventory Domain //! //! Управляет резервированием, проверкой доступности и переносом товара. use crate::shared::{ProductId, Quantity, WarehouseId, ReservationId, Money, Address}; use uuid::Uuid; #[derive(Debug, Clone)] pub struct ReserveRequest { pub items: Vec<(ProductId, Quantity)>, pub warehouse_id: WarehouseId, pub priority: Priority, } #[derive(Debug, Clone, serde::Serialize)] pub struct ItemAvailability { pub product_id: ProductId, pub available: Quantity, pub warehouse_id: WarehouseId, } #[derive(Debug, Clone)] pub struct TransferRequest { pub reservation_id: ReservationId, pub from: WarehouseId, pub to: WarehouseId, } #[derive(Debug, Clone, PartialEq)] pub enum Priority { Low, Normal, High, } // ————————————— // Функции // ————————————— pub fn check_availability(items: Vec<(ProductId, Quantity)>) -> Vec<ItemAvailability> { items.into_iter() .map(|(id, _)| ItemAvailability { product_id: id, available: 10, // упрощение warehouse_id: Uuid::new_v4().into(), }) .collect() } pub fn reserve(request: ReserveRequest) -> Result<ReservationId, InventoryError> { Ok(Uuid::new_v4()) } pub fn cancel_reservation(reservation_id: ReservationId) -> Result<Vec<(ProductId, Quantity)>, InventoryError> { Ok(vec![(Uuid::new_v4(), 2)]) } pub fn transfer_reservation(request: TransferRequest) -> Result<(), InventoryError> { Ok(()) } pub fn confirm_reservation(reservation_id: ReservationId) -> Result<(), InventoryError> { Ok(()) } // ————————————— // Ошибки // ————————————— #[derive(Debug, thiserror::Error)] pub enum InventoryError { #[error("Товар не найден")] NotFound, #[error("Недостаточно на складе")] OutOfStock, } ``` --- ### `src/domain/delivery/src/lib.rs` ```rust //! # Delivery Domain //! //! Управляет доставкой, расчётом, отменой, возвратом. use crate::shared::{Address, Weight, Money, DeliveryId, ReservationId, OrderId, WarehouseId, CustomerId}; use uuid::Uuid; use time::OffsetDateTime; #[derive(Debug, Clone)] pub struct DeliveryRequest { pub address: Address, pub items: Vec<(ProductId, Weight)>, pub priority: Priority, } #[derive(Debug, Clone, serde::Serialize)] pub struct ShippingOptions { pub cost: Money, pub warehouse_id: WarehouseId, pub estimated_days: u32, pub carrier: Carrier, } #[derive(Debug, Clone, serde::Serialize)] pub struct CancellationFee { pub amount: Money, pub reason: String, } #[derive(Debug, Clone, serde::Serialize)] pub struct ReturnLabel { pub tracking_number: String, pub carrier: Carrier, pub expires_at: OffsetDateTime, } #[derive(Debug, Clone, PartialEq)] pub enum Carrier { DHL, FedEx, UPS, } #[derive(Debug, Clone, PartialEq)] pub enum Priority { Low, Normal, High, } #[derive(Debug, Clone, PartialEq)] pub enum DeliveryStatus { Pending, InTransit, Delivered, } // ————————————— // Функции // ————————————— pub fn calculate_shipping(request: DeliveryRequest) -> Result<ShippingOptions, DeliveryError> { Ok(ShippingOptions { cost: rust_decimal_macros::dec!(10.00), warehouse_id: Uuid::new_v4(), estimated_days: 3, carrier: Carrier::DHL, }) } pub fn schedule_delivery(order_id: OrderId, option: ShippingOptions) -> Result<DeliveryId, DeliveryError> { Ok(Uuid::new_v4()) } pub fn cancel_delivery(delivery_id: DeliveryId) -> Result<CancellationFee, DeliveryError> { Ok(CancellationFee { amount: rust_decimal_macros::dec!(5.00), reason: "Early cancellation fee".to_string(), }) } pub fn create_return_label(delivery_id: DeliveryId) -> Result<ReturnLabel, DeliveryError> { Ok(ReturnLabel { tracking_number: "RTN123456789".to_string(), carrier: Carrier::DHL, expires_at: OffsetDateTime::now_utc() + time::Duration::days(7), }) } pub fn get_status(delivery_id: DeliveryId) -> Result<DeliveryStatus, DeliveryError> { Ok(DeliveryStatus::Pending) } // ————————————— // Ошибки // ————————————— #[derive(Debug, thiserror::Error)] pub enum DeliveryError { #[error("Доставка не найдена")] NotFound, #[error("Нельзя отменить доставленный заказ")] CannotCancelDelivered, } ``` --- ### `src/domain/loyalty/src/lib.rs` ```rust //! # Loyalty Domain //! //! Управляет начислением, списанием, заморозкой баллов. use crate::shared::{CustomerId, Money}; use time::OffsetDateTime; use uuid::Uuid; #[derive(Debug, Clone)] pub struct PointsRequest { pub customer_id: CustomerId, pub order_total: Money, } #[derive(Debug, Clone, serde::Serialize)] pub struct LoyaltyPoints { pub amount: u32, pub tier_multiplier: f32, pub expires_at: OffsetDateTime, } #[derive(Debug, Clone)] pub struct PointsTransaction { pub customer_id: CustomerId, pub points: i32, pub reason: String, } // ————————————— // Функции // ————————————— pub fn calculate_points(request: PointsRequest) -> LoyaltyPoints { let amount = (request.order_total.to_f32().unwrap() * 0.01) as u32; LoyaltyPoints { amount, tier_multiplier: 1.0, expires_at: OffsetDateTime::now_utc() + time::Duration::days(365), } } pub fn apply_points(customer_id: CustomerId, points: u32) -> Result<Money, LoyaltyError> { Ok(Money::from(points / 10)) } pub fn rollback_points(transaction: PointsTransaction) -> Result<(), LoyaltyError> { Ok(()) } pub fn freeze_points(customer_id: CustomerId, points: u32, duration: std::time::Duration) -> Result<FreezeId, LoyaltyError> { Ok(Uuid::new_v4()) } pub fn unfreeze_points(freeze_id: FreezeId) -> Result<(), LoyaltyError> { Ok(()) } // ————————————— // Ошибки // ————————————— #[derive(Debug, thiserror::Error)] pub enum LoyaltyError { #[error("Баллы не найдены")] NotFound, #[error("Недостаточно баллов")] InsufficientPoints, } ``` --- ### `src/application/coordination/events.rs` ```rust //! # События //! //! Передаются в CrossDomainCoordinator. //! //! Все типы используют только shared:: и доменные типы. use crate::shared::*; #[derive(Debug, Clone)] pub struct OrderCancelledEvent { pub order_id: OrderId, pub customer_id: CustomerId, pub delivery_id: DeliveryId, pub reservation_id: ReservationId, pub points: u32, pub total: Money, pub delivery_status: DeliveryStatus, } #[derive(Debug, Clone)] pub struct PaymentFailedEvent { pub order_id: OrderId, pub customer_id: CustomerId, pub reservation_id: ReservationId, pub delivery_id: DeliveryId, pub points: u32, pub amount: Money, pub attempt_number: u8, pub failure_reason: PaymentError, } #[derive(Debug, Clone)] pub struct InventoryShortageEvent { pub order_id: OrderId, pub warehouse_id: WarehouseId, pub items: Vec<(ProductId, Quantity)>, pub reservation_id: ReservationId, pub address: Address, pub original_shipping_cost: Money, } #[derive(Debug, Clone)] pub struct PaymentRetryEvent { pub reservation_id: ReservationId, pub delivery_id: DeliveryId, pub customer_id: CustomerId, pub freeze_id: FreezeId, } #[derive(Debug, Clone)] pub struct OrderCancelledCompleteEvent { pub order_id: OrderId, pub refund_id: RefundId, pub returned_items: Vec<(ProductId, Quantity)>, pub cancellation_fee: Money, } ``` --- ### `src/application/coordination/mod.rs` ```rust //! # CrossDomainCoordinator //! //! Центр сложной координации. //! //! ## Почему здесь сосредоточена сложность: //! //! - **Система координационно сложная, а не доменно-сложная**: //! - Домены — простые, стабильные. //! - Сложность — в политике отката, параллелизме, событиях. //! //! - **Много кросс-доменных политик**: //! - При отмене: проверить статус доставки, создать возврат, откатить баллы. //! - При ошибке платежа: заморозить баллы, отложить доставку. //! //! - **Event-driven поведение**: //! - Retry, hold, unfreeze — управляются через события. //! //! → Поэтому `CrossDomainCoordinator` — это **ядро системы**. use std::sync::Arc; use crate::application::coordination::events::*; use crate::domain::pricing; use crate::domain::inventory; use crate::domain::delivery; use crate::domain::loyalty; pub struct CrossDomainCoordinator { pub pricing: Arc<dyn PricingService>, pub inventory: Arc<dyn InventoryService>, pub delivery: Arc<dyn DeliveryService>, pub loyalty: Arc<dyn LoyaltyService>, pub event_bus: Arc<dyn EventBus>, } impl CrossDomainCoordinator { pub fn new( pricing: Arc<dyn PricingService>, inventory: Arc<dyn InventoryService>, delivery: Arc<dyn DeliveryService>, loyalty: Arc<dyn LoyaltyService>, event_bus: Arc<dyn EventBus>, ) -> Self { Self { pricing, inventory, delivery, loyalty, event_bus } } pub async fn handle_order_cancelled(&self, event: OrderCancelledEvent) -> Result<(), Box<dyn std::error::Error>> { let delivery_status = self.delivery.get_status(event.delivery_id).await?; let cancellation_fee = match delivery_status { delivery::DeliveryStatus::Pending => Money::zero(), delivery::DeliveryStatus::InTransit => { let fee = self.delivery.cancel_delivery(event.delivery_id).await?; let label = self.delivery.create_return_label(event.delivery_id).await?; self.notify_customer(event.customer_id, &label).await; fee.amount } delivery::DeliveryStatus::Delivered => return Err("Cannot cancel delivered order".into()), }; let (inv, loy) = tokio::join!( self.inventory.cancel_reservation(event.reservation_id), self.loyalty.rollback_points(loyalty::PointsTransaction { customer_id: event.customer_id, points: -(event.points as i32), reason: format!("Order {} cancelled", event.order_id), }) ); let refund_amount = event.total - cancellation_fee; let refund_id = self.pricing.refund_payment(pricing::RefundRequest { order_id: event.order_id, amount: refund_amount, reason: pricing::RefundReason::CustomerCancelled, }).await?; self.event_bus.publish(OrderCancelledCompleteEvent { order_id: event.order_id, refund_id, returned_items: inv?, cancellation_fee, }).await?; Ok(()) } pub fn handle_payment_failed(&self, event: PaymentFailedEvent) -> Result<(), Box<dyn std::error::Error>> { self.inventory.soft_release(event.reservation_id)?; self.delivery.put_on_hold(event.delivery_id)?; self.loyalty.freeze_points(event.customer_id, event.points, std::time::Duration::from_secs(86400))?; if event.attempt_number > 1 { self.inventory.cancel_reservation(event.reservation_id)?; self.delivery.cancel_delivery(event.delivery_id)?; self.loyalty.rollback_points(loyalty::PointsTransaction { customer_id: event.customer_id, points: -(event.points as i32), reason: "Payment failed after multiple attempts".to_string(), })?; } Ok(()) } pub fn handle_payment_retry(&self, event: PaymentRetryEvent) -> Result<(), Box<dyn std::error::Error>> { self.inventory.restore_reservation(event.reservation_id)?; self.delivery.remove_hold(event.delivery_id)?; self.loyalty.unfreeze_points(event.freeze_id)?; Ok(()) } async fn notify_customer(&self, _customer_id: CustomerId, _label: &delivery::ReturnLabel) { tracing::info!("Возвратная накладная отправлена"); } } ``` --- ### `src/infrastructure/web/handlers.rs` (фрагмент) ```rust pub async fn create_order( State(state): State<SharedState>, Json(req): Json<CreateOrderRequest>, ) -> Result<Json<serde_json::Value>, (StatusCode, String)> { let customer_id: CustomerId = req.customer_id.parse().map_err(|_| (StatusCode::BAD_REQUEST, "Invalid"))?; let items: Vec<(ProductId, Quantity)> = req.items .into_iter() .map(|(id, q)| Ok((id.parse()?, q))) .collect::<Result<_, _>>() .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid"))?; let address = req.address.into(); let order_id = state.create_order_use_case .execute(customer_id, items, address, None) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(json!({ "order_id": order_id.to_string() }))) } // CreateOrderInteractor // application/use_cases/create_order.rs use std::sync::Arc; use crate::application::ports::*; pub trait CreateOrderUseCase: Send + Sync { async fn execute( &self, customer_id: CustomerId, items: Vec<(ProductId, Quantity)>, address: Address, use_loyalty_points: Option<u32>, ) -> Result<OrderId>; } pub struct CreateOrderInteractor { pricing: Arc<dyn PricingService>, inventory: Arc<dyn InventoryService>, delivery: Arc<dyn DeliveryService>, loyalty: Arc<dyn LoyaltyService>, repository: Arc<dyn OrderRepository>, } impl CreateOrderInteractor { pub fn new( pricing: Arc<dyn PricingService>, inventory: Arc<dyn InventoryService>, delivery: Arc<dyn DeliveryService>, loyalty: Arc<dyn LoyaltyService>, repository: Arc<dyn OrderRepository>, ) -> Self { Self { pricing, inventory, delivery, loyalty, repository } } } #[async_trait] impl CreateOrderUseCase for CreateOrderInteractor { async fn execute(...) -> Result<OrderId> { // ... логика создания заказа } } // main.rs use std::sync::Arc; // 1. Создаём реализации портов (adapters) let pricing = Arc::new(MockPricingService); let inventory = Arc::new(MockInventoryService); let delivery = Arc::new(MockDeliveryService); let loyalty = Arc::new(MockLoyaltyService); let repository = Arc::new(InMemoryOrderRepository::new()); let event_bus = Arc::new(MockEventBus); // 2. Создаём координатор let coordinator = Arc::new(CrossDomainCoordinator::new( pricing.clone(), inventory.clone(), delivery.clone(), loyalty.clone(), event_bus, )); // 3. Создаём use case с зависимостями let create_order_use_case: Arc<dyn CreateOrderUseCase> = Arc::new(CreateOrderInteractor::new( pricing, inventory, delivery, loyalty, repository, )); let state = Arc::new(ApplicationState { create_order_use_case, cancel_order_use_case, confirm_payment_use_case, retry_payment_use_case, }); let app = Router::new() .route("/orders", post(handlers::create_order)) .with_state(state); // ← Axum передаст state в обработчики // ApplicationState // src/infrastructure/web/state.rs pub struct ApplicationState { pub create_order_use_case: Arc<dyn CreateOrderUseCase>, pub cancel_order_use_case: Arc<dyn CancelOrderUseCase>, // ... другие use cases } pub type SharedState = Arc<ApplicationState>; // state.create_order_use_case // infrastructure/web/handlers.rs pub async fn create_order( State(state): State<SharedState>, // ← получаем state Json(req): Json<CreateOrderRequest>, ) -> Result<Json<Value>, (StatusCode, String)> { // Используем use case let order_id = state.create_order_use_case .execute(customer_id, items, address, None) .await?; Ok(Json(json!({ "order_id": order_id.to_string() }))) }
TL;DR: мотайте код сразу до слова «coordination»
На что это похоже?
-
В ООП аналогом является паттерн «чистая выдумка»
-
В ФП аналогом является просто набор функций, берущих на себя зависимости
Отдельно хочу заметить, что на Rust доменная логика проектируется и выражается очень красиво!
Немного философии, занудных определений и сравнений с типовыми паттернами
Что такое «междоменный инвариант»?
-
Обычный инвариант:
«Цена не может быть отрицательной», «Нельзя зарезервировать больше, чем есть»
-
Междоменный инвариант:
«Если доставка в пути — нельзя просто отменить заказ, нужно создать возвратную накладную»
«При первой ошибке платежа — заморозить баллы, при второй — отменить всё»
«Если товара нет — найти альтернативный склад, пересчитать доставку, спросить клиента, если дороже» -
Определение:
«Междоменный (процессный) инвариант — это бизнес-правило, которое зависит от состояния нескольких доменов и управляет их взаимодействием во времени.»
Почему классическая архитектура не помогает
-
DDD учит: «всё в домене», «use case — тонкий», «никаких кросс-доменных вызовов»
-
Но:
-
Нет
OrderIdу процесса отмены -
Нет агрегата для «платежа с retry»
-
Нельзя положить политику в
Order::cancel()— она зависит отDeliveryStatus,PaymentAttempts,LoyaltyPoints
-
-
Проблема:
Архитектура описывает сущности, но не процессы.
-
Вывод:
Междоменные инварианты — это не ошибка проектирования, а признак зрелой системы.
Куда девать эту логику?
Разбор антипаттернов и альтернатив:
|
Попытка |
Почему не работает |
|---|---|
|
Положить в |
|
|
Положить в use case |
Use case становится «толстым», дублируется логика |
|
Размазать по доменам |
Каждый делает свою часть — но никто не отвечает за целостность |
|
Создать |
Это не домен, это оркестратор — но в ООП мы называем это «сервис» и считаем второстепенным |
Решение:
Нужно явно выделить процессный домен — область ответственности для управления жизненным циклом.
Процессный домен: новый тип домена
-
Определение:
Процессный домен — это область знаний, отвечающая за жизненные циклы, переходы состояний и кросс-доменные политики.
-
Отличия от классического домена:
Критерий
Сущностный домен
Процессный домен
Центр
Сущность (
Order)Процесс (
OrderCancellation)Идентификатор
Есть (
OrderId)Нет (или вторичен)
Хранение
Агрегат, БД
Временные действия, события
Экспертиза
Бизнес-аналитики
Инженеры, SRE, процессные аналитики
Реализация
Entity, Aggregate
Набор функций, оркестратор
-
Примеры процессных доменов:
-
Управление жизненным циклом заказа
-
Платёж с повторными попытками
-
Возврат товара
-
Интеграция с внешними системами
-
Как реализовать процессный домен?
Варианты:
-
Оркестратор (в монолите)→
CrossDomainCoordinator, функции с явными зависимостями -
Отдельный микросервис→
order-orchestrator,payment-retry-service -
Workflow Engine→ Temporal, Cadence, AWS Step Functions
ссылка на оригинал статьи https://habr.com/ru/articles/937380/
Добавить комментарий