Domain-Driven Design: стратегические паттерны
Зачем знать: DDD — это не про “сложный код”, а про общий язык бизнеса и разработки. Strategic DDD (bounded contexts, context maps, ubiquitous language) определяет границы микросервисов и API. Tactical DDD (aggregates, value objects, domain events) — это код. Middle Go-разработчик должен понимать, когда DDD оправдан (core domain), когда overkill (CRUD приложение), и почему “shared kernel” — это контракт между командами, а “anticorruption layer” — щит от чужого legacy.
Содержание
Заголовок раздела «Содержание»- Концепция: Strategic DDD
- Глубже: Context maps, Event Storming, Tactical DDD
- Gotchas
- Real cases
- Вопросы (25)
- Practice
- Источники
1. Концепция
Заголовок раздела «1. Концепция»1.1 Что такое DDD
Заголовок раздела «1.1 Что такое DDD»Domain-Driven Design — подход к проектированию ПО, где доменная модель отражает бизнес. Введён Eric Evans в книге “Domain-Driven Design: Tackling Complexity in the Heart of Software” (2003).
Два уровня:
- Strategic DDD: макро. Bounded contexts, context maps, ubiquitous language. Где границы сервисов? Кто с кем говорит?
- Tactical DDD: микро. Entities, value objects, aggregates, repositories. Как структурировать код модуля?
DDD ≠ techniques. DDD — это mindset: код должен моделировать domain, а не быть transactional CRUD.
1.2 Ubiquitous Language
Заголовок раздела «1.2 Ubiquitous Language»Единый язык между бизнесом, аналитиками, разработчиками. Термины из домена → термины в коде.
Anti-pattern:
- Бизнес: “Клиент заказал товар.”
- Код:
OrderEntity.userId = customerId; orderItems = lineItems; - Discrepancy → bugs, miscommunication.
Правильно: одни и те же термины везде. Customer (не User), LineItem (не OrderItem). Если бизнес использует “Сделка” — код использует Deal, не Transaction.
Закрепление: glossary, naming в коде, документация.
1.3 Bounded Context
Заголовок раздела «1.3 Bounded Context»Bounded context — границы, внутри которых определённая модель имеет смысл.
Пример: “Customer” в Sales — это потенциальный покупатель с интересами. “Customer” в Billing — это плательщик с счётами и инвойсами. Это два разных Customer, две модели, две команды.
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐│ Sales Context │ │ Billing Context │ │ Support Context ││ │ │ │ │ ││ Customer: │ │ Customer: │ │ Customer: ││ - leads │ │ - invoices │ │ - tickets ││ - interests │ │ - paymentMethods │ │ - history ││ - opportunities │ │ - billingAddress │ │ - satisfaction │└─────────────────────┘ └─────────────────────┘ └─────────────────────┘Bounded context = микросервис (часто). Не всегда: в monolith bounded context может быть модулем.
1.4 Subdomain типы
Заголовок раздела «1.4 Subdomain типы»Domain — вся бизнес-область компании (e-commerce, банк, taxi service). Subdomain — часть domain, отвечающая за конкретную область.
Три типа:
Core subdomain:
- Конкурентное преимущество. То, что делает бизнес уникальным.
- Самая большая investment, лучшие инженеры.
- Custom development.
- Пример: matching algorithm в taxi, recommendation engine в Netflix, search в Google.
Supporting subdomain:
- Поддерживает core, но не differentiates.
- Custom development, но проще.
- Пример: order tracking, inventory management в e-commerce.
Generic subdomain:
- Стандартная функциональность, везде одинаковая.
- Buy off the shelf, don’t build. SaaS, open source.
- Пример: authentication (Auth0, Keycloak), payments (Stripe), email (SendGrid).
⚠️ Правило: investment в core, минимум в generic. Не строй свой CRM, если ты taxi service.
1.5 Context Map
Заголовок раздела «1.5 Context Map»Диаграмма отношений между bounded contexts. Показывает: кто с кем интегрируется, кто доминирует, кто подчиняется.
┌──────────────┐ │ Marketing │ └──────┬───────┘ │ Customer/Supplier ▼┌──────────────┐ ┌──────────────┐ ┌──────────────┐│ Catalog │◀───│ Sales │───▶│ Billing │└──────┬───────┘ └──────────────┘ └──────────────┘ │ Conformist │ ▼ │ ACL┌──────────────┐ ┌─────▼─────────┐│ Legacy ERP │ │ Accounting │└──────────────┘ │ (external) │ └───────────────┘1.6 Context Map Patterns
Заголовок раздела «1.6 Context Map Patterns»Shared Kernel
Заголовок раздела «Shared Kernel»Две команды владеют общим куском модели/кода. Меняется только по соглашению обеих.
Пример: общая модель Money (currency + amount). Изменение требует консультации.
⚠️ Tight coupling. Используй редко.
Customer/Supplier
Заголовок раздела «Customer/Supplier»Upstream (supplier) предоставляет API. Downstream (customer) использует. У downstream есть голос: supplier учитывает требования customer.
Пример: Catalog → Search. Search команда говорит Catalog: “нам нужно поле weight для поиска тяжёлых товаров.” Catalog соглашается.
Conformist
Заголовок раздела «Conformist»Downstream использует API upstream as is, без влияния. Когда upstream — внешняя система или сильнее.
Пример: интеграция с Stripe. Мы используем их модель Customer, Subscription. Не модифицируем.
⚠️ Опасно при big change у upstream — мы должны мигрировать.
Anticorruption Layer (ACL)
Заголовок раздела «Anticorruption Layer (ACL)»Защитный слой между нашей моделью и чужой. Переводит данные туда-сюда.
[Our Domain Model] ←──→ [ACL] ←──→ [External System (e.g. SAP)] ↑ Transformation, prevents pollutionЗачем:
- Не позволяет чужой модели заразить нашу (anemic, transactional).
- Изоляция: меняется SAP — меняется только ACL.
// External SAP modeltype SAPCustomer struct { BPNo string // business partner number CName string // customer name (10 chars max!) Cred float64 // credit limit // ...}
// Our domain modeltype Customer struct { ID CustomerID DisplayName string CreditLimit Money}
// ACLtype CustomerACL struct { sap *SAPClient}
func (a *CustomerACL) GetByID(ctx context.Context, id CustomerID) (*Customer, error) { sapCust, err := a.sap.FetchCustomer(ctx, string(id)) if err != nil { return nil, err } return &Customer{ ID: CustomerID(sapCust.BPNo), DisplayName: strings.TrimSpace(sapCust.CName), CreditLimit: NewMoney(sapCust.Cred, USD), }, nil}Open Host Service
Заголовок раздела «Open Host Service»Сервис предоставляет publichный protocol для многих consumers. Контракт чёткий, стабильный.
Пример: REST API e-commerce платформы для партнёров.
Published Language
Заголовок раздела «Published Language»Формализованный язык/формат для интеграции (XML, JSON Schema, Protobuf).
Пример: ISO 20022 (банковские сообщения), HL7 (медицина).
Separate Ways
Заголовок раздела «Separate Ways»Контексты вообще не интегрируются. Каждый делает своё.
Пример: HR system и Inventory — не связаны напрямую.
Big Ball of Mud
Заголовок раздела «Big Ball of Mud»Антипаттерн: нет границ, всё связано со всем. Признак — модули зависят от всех других.
1.7 Tactical DDD: краткое повторение
Заголовок раздела «1.7 Tactical DDD: краткое повторение»См. middle 1 файл о DDD tactical. Кратко:
- Entity: объект с identity. Меняется со временем, но identity persistent.
Order,Customer. - Value Object: immutable, identified by value.
Money,Address,DateRange. - Aggregate: cluster of entities/VOs, treated as unit. Root entity — aggregate root, единственная точка доступа.
- Aggregate Root: консистентность внутри aggregate. Транзакция = один aggregate.
- Repository: хранилище aggregates, маскирует БД. Interface в domain, implementation в infrastructure.
- Domain Service: логика, которая не принадлежит ни одной entity.
PricingService. - Domain Event: что произошло в domain.
OrderPlaced. - Factory: сложная конструкция aggregates.
1.8 DDD в Go: структура папок
Заголовок раздела «1.8 DDD в Go: структура папок»Clean architecture + DDD:
project/├── cmd/│ └── app/main.go├── internal/│ ├── domain/ # ядро, нет зависимостей от infrastructure│ │ ├── order/│ │ │ ├── order.go # Aggregate root│ │ │ ├── line_item.go # Entity внутри aggregate│ │ │ ├── money.go # Value object│ │ │ ├── repository.go # Interface│ │ │ ├── service.go # Domain service│ │ │ └── events.go # Domain events│ │ ├── customer/│ │ └── payment/│ ├── application/ # use cases, application services│ │ ├── order/│ │ │ ├── create_order.go # use case│ │ │ └── refund_order.go│ │ └── shared/│ ├── infrastructure/ # adapters│ │ ├── persistence/│ │ │ └── postgres/│ │ │ └── order_repository.go # implements domain.Repository│ │ ├── messaging/│ │ └── http/│ └── interfaces/│ ├── http/│ │ └── handlers/│ └── grpc/Правила:
- Domain не зависит от ничего (no
database/sql, nokafka). - Application использует domain interfaces.
- Infrastructure реализует interfaces.
- Dependency inversion: domain декларирует, infrastructure реализует.
1.9 No Anemic Domain Model
Заголовок раздела «1.9 No Anemic Domain Model»Anemic domain model: объекты — только getters/setters. Поведение — в “service” классах. Это процедурное программирование, не OOP.
// ANEMICtype Order struct { ID string Status string Items []Item Total int64}
// Все методы вне Orderfunc (s *OrderService) Place(o *Order) { o.Status = "placed" }func (s *OrderService) Ship(o *Order) { o.Status = "shipped" }func (s *OrderService) Cancel(o *Order) { o.Status = "cancelled" }Проблема: бизнес-логика разбросана. Inconsistent states легко создать.
// RICH domaintype Order struct { id OrderID status OrderStatus items []LineItem total Money}
func (o *Order) Place() error { if o.status != Draft { return ErrInvalidTransition } if len(o.items) == 0 { return ErrEmptyOrder } o.status = Placed return nil}
func (o *Order) Ship() error { if o.status != Paid { return ErrNotPaid } o.status = Shipped return nil}Логика и правила инкапсулированы в Order. Невозможно создать Order{status: shipped, items: []}.
1.10 Constructor validation
Заголовок раздела «1.10 Constructor validation»func NewOrder(customerID CustomerID, items []LineItem) (*Order, error) { if customerID == "" { return nil, ErrCustomerRequired } if len(items) == 0 { return nil, ErrEmptyOrder } total := Money{} for _, item := range items { total = total.Add(item.Subtotal()) } return &Order{ id: NewOrderID(), customerID: customerID, items: items, total: total, status: Draft, }, nil}Invariant: Order не может существовать с пустыми items. Constructor — gatekeeper.
1.11 Когда DDD не нужен
Заголовок раздела «1.11 Когда DDD не нужен»⚠️ Простой CRUD приложение: DDD overhead не оправдан. Делай простую трёхслойку.
⚠️ MVP / prototype: скорость важнее правильности модели.
⚠️ Generic subdomain: не моделируй email service по DDD — Just use SendGrid.
DDD оправдан:
- Core subdomain.
- Complex business rules (insurance, banking, healthcare).
- Long-living системы (10+ лет).
- Multiple stakeholders с разными view (нужен ubiquitous language).
2. Глубже
Заголовок раздела «2. Глубже»2.1 Event Storming (Alberto Brandolini)
Заголовок раздела «2.1 Event Storming (Alberto Brandolini)»Workshop technique для discovery domain. Все stakeholders вместе. Sticky notes на стене.
Подготовка:
- Длинная стена (4-10 метров обоев).
- Sticky notes разных цветов.
- Маркеры.
- Все ключевые люди (бизнес, разработчики, аналитики).
Цвета:
- 🟠 Orange — Domain Events (“OrderPlaced”, “PaymentReceived”).
- 🟣 Purple — Hotspots / Questions (где неясно).
- 🔵 Blue — Commands (“PlaceOrder”).
- 🟡 Yellow — Actors / Users.
- 🟢 Green — Read models / Views.
- 🔴 Red — Policies / Business rules.
- 🟣 Lilac — Aggregates.
Этапы:
Big Picture Event Storming (1-2 часа)
Заголовок раздела «Big Picture Event Storming (1-2 часа)»- Все участники “штормят” events на стену в хронологическом порядке.
- “Что произошло в системе?” — past tense.
- Цель: общая картина flow.
[OrderCreated] → [PaymentReceived] → [InventoryReserved] → [OrderShipped]Process-Level Event Storming
Заголовок раздела «Process-Level Event Storming»- Добавить commands, actors, policies.
- Customer (yellow) → PlaceOrder (blue) → OrderCreated (orange).
- Найти hotspots (purple) — где недопонимание.
Software Design (Bounded contexts)
Заголовок раздела «Software Design (Bounded contexts)»- Группировать события по контекстам.
- Identifies aggregate boundaries.
- Назначить ответственность.
2.2 Find bounded contexts via Event Storming
Заголовок раздела «2.2 Find bounded contexts via Event Storming»После Big Picture session:
- Группировать events по принадлежности: какие события “вместе”? Tema?
- Свободные группы = candidate bounded contexts.
- Линии между группами = integration points (context map).
Пример output:
Sales context: - LeadCreated - QuoteSent - DealClosed
Order context: - OrderPlaced - OrderConfirmed - OrderCancelled
Fulfillment context: - InventoryReserved - PackingCompleted - ShipmentScheduled
Billing context: - InvoiceCreated - PaymentReceived - RefundIssuedМежду ними — integration (events). Например, OrderPlaced → fulfillment слушает.
2.3 Strategic DDD на примере e-commerce
Заголовок раздела «2.3 Strategic DDD на примере e-commerce»Domain: интернет-магазин.
Subdomains:
- Core: Catalog (search, recommendations), Pricing (dynamic, promotions), Checkout (UX, conversion).
- Supporting: Order management, Inventory, Customer service.
- Generic: Authentication, Payment processing (Stripe), Email (SendGrid), Shipping (FedEx API).
Investment matrix:
- Catalog — большая команда, custom search, ML recommendations.
- Order management — средняя команда, custom но не уникально.
- Auth — Auth0/Keycloak, минимум кода.
Context map:
- Catalog ←→ Order (customer/supplier: Order требует API от Catalog).
- Order ←→ Payment (ACL: Payment — Stripe wrapper).
- Customer ←→ Order (shared kernel: Customer ID common).
2.4 DDD vs Anemic Domain Model
Заголовок раздела «2.4 DDD vs Anemic Domain Model»Anemic (Spring/Hibernate стиль):
class Order { private String id; private List<Item> items; private Status status; // getters/setters}
class OrderService { public void place(Order o) { o.setStatus(PLACED); repo.save(o); eventBus.publish(new OrderPlaced(o.getId())); }}Логика в OrderService, Order — DTO. Martin Fowler — “Anemic Domain Model anti-pattern”.
Rich domain:
type Order struct { id OrderID items []LineItem status OrderStatus}
func (o *Order) Place() ([]DomainEvent, error) { if o.status != Draft { return nil, ErrInvalidTransition } if len(o.items) == 0 { return nil, ErrEmptyOrder } o.status = Placed return []DomainEvent{OrderPlaced{OrderID: o.id}}, nil}Логика и invariant в Order. Service вызывает domain method.
2.5 Aggregate boundaries
Заголовок раздела «2.5 Aggregate boundaries»Правила:
- Транзакция = один aggregate. Между aggregates — eventual consistency.
- Reference между aggregates через ID, не объект (lazy loading избежать).
- Aggregate small as possible. Большой aggregate — performance проблемы и lock contention.
Пример:
// Order aggregatetype Order struct { id OrderID customerID CustomerID // reference, not Customer object items []LineItem // inside aggregate status OrderStatus}
// Customer aggregate (separate)type Customer struct { id CustomerID name string}В транзакции меняется или Order, или Customer — не оба. Между ними — events.
2.6 Repository interface в domain
Заголовок раздела «2.6 Repository interface в domain»package order
import "context"
type Repository interface { Save(ctx context.Context, order *Order) error FindByID(ctx context.Context, id OrderID) (*Order, error) FindByCustomer(ctx context.Context, customerID CustomerID) ([]*Order, error)}Реализация в infrastructure:
package postgres
type OrderRepository struct { db *pgxpool.Pool}
func (r *OrderRepository) Save(ctx context.Context, o *order.Order) error { // SQL query}
func (r *OrderRepository) FindByID(ctx context.Context, id order.OrderID) (*order.Order, error) { // SQL query, reconstruct aggregate}Dependency inversion: domain декларирует, infrastructure реализует.
2.7 Domain events
Заголовок раздела «2.7 Domain events»type DomainEvent interface { AggregateID() string EventType() string OccurredAt() time.Time}
type OrderPlaced struct { OrderID OrderID CustomerID CustomerID Total Money Timestamp time.Time}
func (e OrderPlaced) AggregateID() string { return string(e.OrderID) }func (e OrderPlaced) EventType() string { return "order.placed" }func (e OrderPlaced) OccurredAt() time.Time { return e.Timestamp }Aggregate генерирует events:
func (o *Order) Place() error { o.status = Placed o.events = append(o.events, OrderPlaced{...}) return nil}
func (o *Order) PullEvents() []DomainEvent { events := o.events o.events = nil return events}Application service публикует events после Save (Outbox).
2.8 Domain service vs Application service
Заголовок раздела «2.8 Domain service vs Application service»Domain service: логика, которая не принадлежит ни одной entity, но является частью domain.
type PricingService struct{}
func (s *PricingService) CalculateOrderTotal(items []LineItem, customer Customer, promotion Promotion) Money { // Сложная логика расчёта}Application service (use case): orchestrates domain. Не имеет бизнес-логики.
type CreateOrderUseCase struct { orderRepo order.Repository customerRepo customer.Repository pricing *PricingService events EventBus}
func (uc *CreateOrderUseCase) Execute(ctx context.Context, cmd CreateOrderCmd) error { c, err := uc.customerRepo.FindByID(ctx, cmd.CustomerID) if err != nil { return err }
items := buildLineItems(cmd.Items) order, err := order.NewOrder(c.ID, items) if err != nil { return err }
if err := order.Place(); err != nil { return err } if err := uc.orderRepo.Save(ctx, order); err != nil { return err }
for _, e := range order.PullEvents() { uc.events.Publish(ctx, e) } return nil}2.9 Factory pattern
Заголовок раздела «2.9 Factory pattern»Для сложной конструкции aggregate:
package order
type Factory struct { pricing *PricingService catalog catalog.Service}
func (f *Factory) CreateFromCart(ctx context.Context, cart Cart) (*Order, error) { items := []LineItem{} for _, ci := range cart.Items { product, err := f.catalog.GetProduct(ctx, ci.ProductID) if err != nil { return nil, err } item := NewLineItem(product.ID, product.Name, product.Price, ci.Quantity) items = append(items, item) } total := f.pricing.Calculate(items, cart.PromotionCode) return NewOrder(cart.CustomerID, items, total)}2.10 Practical adoption: не DDD везде
Заголовок раздела «2.10 Practical adoption: не DDD везде»⚠️ DDD overhead значительный. Не применяй на:
- Generic subdomains (auth, email).
- CRUD admin panels.
- Simple lookups (countries list, currency list).
Применяй на:
- Core subdomains (где деньги делаются).
- Complex business rules (insurance, lending, taxation).
- Long-living systems.
Гибридный подход:
- Core — rich DDD.
- Supporting — simplified DDD.
- Generic — quick CRUD или integrate SaaS.
2.11 Tradeoffs DDD
Заголовок раздела «2.11 Tradeoffs DDD»Pros:
- Чёткая модель, понятная бизнесу.
- Ubiquitous language снижает miscommunication.
- Easier refactoring (логика в domain, не разбросана).
- Тестируемость (domain без infrastructure).
- Maintenance over years.
Cons:
- Learning curve.
- Overhead для simple cases.
- Время на event storming, modeling sessions.
- Команда должна понимать principles (иначе деградация).
3. Gotchas
Заголовок раздела «3. Gotchas»⚠️ DDD ≠ tactical patterns only. Многие думают “я использую aggregate, значит у меня DDD.” Без strategic (bounded contexts, ubiquitous language) — это просто tactical patterns.
⚠️ Bounded context = микросервис? Часто, но не всегда. В modular monolith — bounded context — module. Не дроби сервисы только потому, что у тебя bounded context.
⚠️ Shared kernel опасен. Tight coupling. Изменение требует координации. Минимизируй.
⚠️ Conformist рискован. Если upstream — внешняя система (Stripe), и она меняется — мы должны мигрировать. ACL для критических интеграций.
⚠️ Anemic model легко скатываются. Без vigilance команда добавляет setters, и rich domain становится anemic. Code review критичен.
⚠️ Большие aggregates. Order с 10000 LineItems — performance catastrophe. Разбивай: Order Header + LineItems как отдельный aggregate, ссылка через ID.
⚠️ Cross-aggregate consistency. Хочешь в одной транзакции изменить 2 aggregates — это смell. Либо они один aggregate, либо eventual consistency через events.
⚠️ Ubiquitous language игнорируется. Бизнес говорит “сделка”, код пишет “Transaction”. Через год — bugs из-за miscommunication. Glossary + энфорсмент в код.
⚠️ Event storming = бесплатно сегодня, выгодно завтра. Workshop на 2 часа экономит месяцы переделок. Делай для нового модуля или major рефакторинга.
⚠️ Repository как DAO. Если repository имеет FindByEmailAndStatusAndCreatedAfter — это leakage из БД, не domain интерфейс. Domain operations должны иметь смысл в domain.
⚠️ Domain services overuse. Если всё в domain service, а entities anemic — это anemic с другим названием. Логика должна быть в entities когда возможно.
⚠️ DTOs в domain. Не возвращай OrderDTO из domain. Domain работает с aggregates. DTOs — на application/interface boundary.
⚠️ ORM в domain. GORM tags gorm:"primaryKey" в domain entity → coupling. Используй mapper в repository.
⚠️ Event-driven без bounded contexts. Если publish events без чёткого ownership, кто owns event? Чей contract? Хаос.
⚠️ DDD для CRUD. Если у тебя 50 таблиц и каждая — простой CRUD, DDD — overkill. Используй active record или simple service layer.
⚠️ “Big bang” DDD adoption. Введение DDD на legacy system без подготовки команды → провал. Start small, один bounded context, обучение.
4. Real cases
Заголовок раздела «4. Real cases»PayPal — DDD для финансовых операций. Внутренние системы PayPal используют DDD. Aggregate “Transaction” с invariants (amount > 0, currency consistent). Multiple bounded contexts: payments, disputes, fraud detection.
Salesforce — DDD-inspired core. Core CRM domain (Account, Opportunity, Lead) — rich modeling. Bounded contexts: Sales Cloud, Service Cloud, Marketing Cloud.
Insurance / banking domains. Domain-rich industries. Allianz, ING используют DDD для рисков и compliance. Strategic DDD определяет microservice boundaries.
Yandex.Eda — order pipeline. Bounded contexts: Order, Restaurant, Courier, Pricing. Events между ними. Strategic DDD заметна в архитектуре.
Avito — listings. Core domain — Listing (объявление). Sub-contexts: Pricing, Moderation, Search. Каждый — отдельная команда.
Vaughn Vernon’s IDDD example: “Implementing Domain-Driven Design” — практический пример SaaSOvation. Идея организованной DDD-системы.
Sportify — Squad model + DDD. Squads (small teams) владеют subdomains. Event-driven между ними. Ubiquitous language в каждом squad.
Tinkoff Bank — banking DDD. Сложные финансовые domain. Account, Transaction, Loan — aggregates. ACL для интеграции с central bank, SWIFT.
Booking.com — pricing engine. Core subdomain. Custom built. Rich domain modeling, complex business rules.
Uber — pricing surge. Core: matching и pricing. Bounded contexts: Trip, Pricing, Driver, Payment. Events между ними.
Trivago — search funnel. Bounded contexts: Search, Booking, Hotel data. Events для analytics.
Microsoft — eShopOnContainers reference architecture. Открытый пример microservices+DDD на .NET. Aggregates, domain events, bounded contexts.
Domain Storytelling. Альтернативная Event Storming методика (Stefan Hofer). Strory-based. Используется в DDD проектах.
EventModeling. Связанная Event Storming, более структурированная. Adam Dymitruk.
Cars24, OYO. Indian unicorns используют DDD + microservices для complex marketplaces.
5. Вопросы
Заголовок раздела «5. Вопросы»Q1: Что такое Domain-Driven Design? A: Подход к проектированию ПО, где доменная модель отражает бизнес. Eric Evans, 2003. Strategic (bounded contexts, ubiquitous language) + Tactical (aggregates, value objects).
Q2: В чём разница strategic и tactical DDD? A: Strategic — макро: bounded contexts, context map, ubiquitous language. Где границы сервисов? Tactical — микро: entities, aggregates, repositories. Как структурировать код.
Q3: Что такое ubiquitous language? A: Единый язык между бизнесом и разработчиками. Бизнес-термины — в коде. Glossary, naming в коде, документация. Снижает miscommunication.
Q4: Что такое bounded context? A: Границы, внутри которых модель имеет смысл. “Customer” в Sales ≠ “Customer” в Billing. Часто = микросервис, может быть = module в monolith.
Q5: Какие типы subdomain? A: Core (конкурентное преимущество, custom), Supporting (поддерживает core, custom но проще), Generic (стандартное, buy не build).
Q6: Куда инвестировать команду? A: Core — большая команда, лучшие инженеры. Supporting — средняя. Generic — SaaS/OSS, минимум кода. Не пиши свой CRM, если ты taxi service.
Q7: Что такое context map? A: Диаграмма отношений между bounded contexts. Кто с кем интегрируется, кто доминирует. Patterns: shared kernel, customer/supplier, conformist, ACL, etc.
Q8: Что такое shared kernel? A: Общий кусок модели/кода между двумя контекстами. Изменение требует согласия обеих команд. Tight coupling, использовать осторожно.
Q9: Customer/supplier — что это? A: Upstream (supplier) предоставляет API. Downstream (customer) использует. Downstream имеет голос: supplier учитывает требования.
Q10: Что такое conformist? A: Downstream использует API upstream as is, без влияния. Опасно при big change у upstream. Используется когда upstream — external или сильнее.
Q11: Что такое Anticorruption Layer (ACL)? A: Защитный слой между нашей моделью и чужой. Переводит данные туда-сюда. Не позволяет чужой модели заразить нашу. Используется для внешних интеграций (SAP, legacy).
Q12: Что такое Event Storming? A: Workshop technique для discovery domain. Sticky notes на стене: events, commands, aggregates, policies. Все stakeholders вместе. Find bounded contexts.
Q13: В чём разница Big Picture vs Process-Level Event Storming? A: Big Picture — high level, все events системы в порядке. 1-2 часа. Process-Level — детально один процесс, добавить commands, actors, policies, hotspots.
Q14: Что такое anemic domain model? A: Anti-pattern: entities только с getters/setters, логика — в services. Это procedural в OOP-обёртке. Rich domain — entities с поведением.
Q15: Где должна быть бизнес-логика в DDD? A: В entities/aggregates (rich domain), domain services (когда не принадлежит entity), domain events. Application services — orchestrate, не содержат логики.
Q16: Что такое aggregate? A: Cluster of entities/VOs, treated as unit. Root entity (aggregate root) — единственная точка доступа. Транзакция = один aggregate. Между aggregates — eventual consistency.
Q17: Как ссылаться между aggregates? A: Через ID, не объект. Order ссылается на CustomerID, не на Customer object. Избежать lazy loading, явные boundaries.
Q18: Где живёт Repository interface? A: В domain слое (вместе с entities). Реализация — в infrastructure. Dependency inversion: domain декларирует, infrastructure реализует.
Q19: Domain service vs Application service? A: Domain service — логика, не принадлежащая ни одной entity, часть domain. Application service (use case) — orchestrates domain, не содержит логики.
Q20: Когда DDD не нужен? A: Простой CRUD, MVP/прототип, generic subdomain. DDD overhead — для complex domain (banking, insurance), core subdomains, long-living systems.
Q21: Как Event Storming находит bounded contexts? A: После Big Picture группировать events по теме/принадлежности. Свободные группы = candidate bounded contexts. Линии между = integration points.
Q22: Что такое factory в DDD? A: Pattern для сложной конструкции aggregate. Когда constructor недостаточно (нужно вызывать внешние services). Factory в domain, использует repository/service.
Q23: Как тестировать domain? A: Unit tests без infrastructure (in-memory fakes). Domain не зависит от БД/HTTP. Property-based testing для invariants.
Q24: Что такое factory + repository связка? A: Factory создаёт новые aggregates. Repository достаёт существующие. Оба возвращают aggregate root.
Q25: Какие преимущества DDD? A: Чёткая модель, ubiquitous language, easier refactoring, тестируемость, maintenance долгие годы. Недостатки: learning curve, overhead для simple cases.
6. Practice
Заголовок раздела «6. Practice»-
Event Storming session. Возьми домен (пиццерия, библиотека, банк). С коллегами (или один) разложи events на доске. Найди bounded contexts.
-
Context map for e-commerce. Опиши контексты: Catalog, Order, Payment, Shipping, Customer. Какие patterns используются (ACL, Customer/Supplier, etc.)?
-
Rich domain model. Реализуй
Orderaggregate в Go. State machine (Draft → Placed → Paid → Shipped → Delivered). Invariants. Constructor validation. -
Repository pattern. Define interface в domain, реализуй в postgres. Map aggregate ↔ rows.
-
Domain events. Aggregate генерирует events. Application service вытягивает и публикует через outbox.
-
ACL для internal API. Symuluj два сервиса с разной моделью (наш Customer и SAP customer). ACL переводит туда-сюда.
-
Anti-corruption layer. Symulируй интеграцию с Stripe. Наш Domain Money vs Stripe API. ACL.
-
Subdomain classification. Возьми реальный проект. Classify all features as core/supporting/generic. Где investment overspends на generic?
-
DDD vs anemic. Refactor anemic order to rich. Покажи разницу в bugs (state machine).
-
Bounded contexts vs microservices. Покажи в коде: monolith с 3 bounded contexts через modules. Extract один в microservice. Что меняется?
7. Источники
Заголовок раздела «7. Источники»- Eric Evans. “Domain-Driven Design: Tackling Complexity in the Heart of Software.” Addison-Wesley, 2003. — оригинал, must read.
- Vaughn Vernon. “Implementing Domain-Driven Design.” Addison-Wesley, 2013. — практическое продолжение.
- Vaughn Vernon. “Domain-Driven Design Distilled.” 2016. — сжатая версия.
- Eric Evans. “DDD Reference” (free). https://www.domainlanguage.com/ddd/reference/
- Alberto Brandolini. “Introducing EventStorming.” https://www.eventstorming.com/
- Martin Fowler. “AnemicDomainModel.” https://martinfowler.com/bliki/AnemicDomainModel.html
- Martin Fowler. “BoundedContext.” https://martinfowler.com/bliki/BoundedContext.html
- Nick Tune. “Strategic DDD” articles. https://medium.com/@ntcoding
- Scott Millett, Nick Tune. “Patterns, Principles, and Practices of DDD.” Wrox, 2015.
- Domain Language website. https://www.domainlanguage.com/
- Microsoft. ”.NET Microservices Architecture for Containerized .NET Applications.” (eShopOnContainers reference).
- Stefan Hofer. “Domain Storytelling.” 2021.