Перейти к содержимому

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.

  1. Концепция: Strategic DDD
  2. Глубже: Context maps, Event Storming, Tactical DDD
  3. Gotchas
  4. Real cases
  5. Вопросы (25)
  6. Practice
  7. Источники

Domain-Driven Design — подход к проектированию ПО, где доменная модель отражает бизнес. Введён Eric Evans в книге “Domain-Driven Design: Tackling Complexity in the Heart of Software” (2003).

Два уровня:

  1. Strategic DDD: макро. Bounded contexts, context maps, ubiquitous language. Где границы сервисов? Кто с кем говорит?
  2. Tactical DDD: микро. Entities, value objects, aggregates, repositories. Как структурировать код модуля?

DDD ≠ techniques. DDD — это mindset: код должен моделировать domain, а не быть transactional CRUD.

Единый язык между бизнесом, аналитиками, разработчиками. Термины из домена → термины в коде.

Anti-pattern:

  • Бизнес: “Клиент заказал товар.”
  • Код: OrderEntity.userId = customerId; orderItems = lineItems;
  • Discrepancy → bugs, miscommunication.

Правильно: одни и те же термины везде. Customer (не User), LineItem (не OrderItem). Если бизнес использует “Сделка” — код использует Deal, не Transaction.

Закрепление: glossary, naming в коде, документация.

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 может быть модулем.

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.

Диаграмма отношений между bounded contexts. Показывает: кто с кем интегрируется, кто доминирует, кто подчиняется.

┌──────────────┐
│ Marketing │
└──────┬───────┘
│ Customer/Supplier
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Catalog │◀───│ Sales │───▶│ Billing │
└──────┬───────┘ └──────────────┘ └──────────────┘
│ Conformist │
▼ │ ACL
┌──────────────┐ ┌─────▼─────────┐
│ Legacy ERP │ │ Accounting │
└──────────────┘ │ (external) │
└───────────────┘

Две команды владеют общим куском модели/кода. Меняется только по соглашению обеих.

Пример: общая модель Money (currency + amount). Изменение требует консультации.

⚠️ Tight coupling. Используй редко.

Upstream (supplier) предоставляет API. Downstream (customer) использует. У downstream есть голос: supplier учитывает требования customer.

Пример: Catalog → Search. Search команда говорит Catalog: “нам нужно поле weight для поиска тяжёлых товаров.” Catalog соглашается.

Downstream использует API upstream as is, без влияния. Когда upstream — внешняя система или сильнее.

Пример: интеграция с Stripe. Мы используем их модель Customer, Subscription. Не модифицируем.

⚠️ Опасно при big change у upstream — мы должны мигрировать.

Защитный слой между нашей моделью и чужой. Переводит данные туда-сюда.

[Our Domain Model] ←──→ [ACL] ←──→ [External System (e.g. SAP)]
Transformation,
prevents pollution

Зачем:

  • Не позволяет чужой модели заразить нашу (anemic, transactional).
  • Изоляция: меняется SAP — меняется только ACL.
// External SAP model
type SAPCustomer struct {
BPNo string // business partner number
CName string // customer name (10 chars max!)
Cred float64 // credit limit
// ...
}
// Our domain model
type Customer struct {
ID CustomerID
DisplayName string
CreditLimit Money
}
// ACL
type 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
}

Сервис предоставляет publichный protocol для многих consumers. Контракт чёткий, стабильный.

Пример: REST API e-commerce платформы для партнёров.

Формализованный язык/формат для интеграции (XML, JSON Schema, Protobuf).

Пример: ISO 20022 (банковские сообщения), HL7 (медицина).

Контексты вообще не интегрируются. Каждый делает своё.

Пример: HR system и Inventory — не связаны напрямую.

Антипаттерн: нет границ, всё связано со всем. Признак — модули зависят от всех других.

См. 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.

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, no kafka).
  • Application использует domain interfaces.
  • Infrastructure реализует interfaces.
  • Dependency inversion: domain декларирует, infrastructure реализует.

Anemic domain model: объекты — только getters/setters. Поведение — в “service” классах. Это процедурное программирование, не OOP.

// ANEMIC
type Order struct {
ID string
Status string
Items []Item
Total int64
}
// Все методы вне Order
func (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 domain
type 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: []}.

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.

⚠️ Простой 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).

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.

Этапы:

  • Все участники “штормят” events на стену в хронологическом порядке.
  • “Что произошло в системе?” — past tense.
  • Цель: общая картина flow.
[OrderCreated] → [PaymentReceived] → [InventoryReserved] → [OrderShipped]
  • Добавить commands, actors, policies.
  • Customer (yellow) → PlaceOrder (blue) → OrderCreated (orange).
  • Найти hotspots (purple) — где недопонимание.
  • Группировать события по контекстам.
  • Identifies aggregate boundaries.
  • Назначить ответственность.

После Big Picture session:

  1. Группировать events по принадлежности: какие события “вместе”? Tema?
  2. Свободные группы = candidate bounded contexts.
  3. Линии между группами = 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 слушает.

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).

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.

Правила:

  1. Транзакция = один aggregate. Между aggregates — eventual consistency.
  2. Reference между aggregates через ID, не объект (lazy loading избежать).
  3. Aggregate small as possible. Большой aggregate — performance проблемы и lock contention.

Пример:

// Order aggregate
type 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.

internal/domain/order/repository.go
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:

internal/infrastructure/persistence/postgres/order_repo.go
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 реализует.

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).

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
}

Для сложной конструкции 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)
}

⚠️ 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.

Pros:

  • Чёткая модель, понятная бизнесу.
  • Ubiquitous language снижает miscommunication.
  • Easier refactoring (логика в domain, не разбросана).
  • Тестируемость (domain без infrastructure).
  • Maintenance over years.

Cons:

  • Learning curve.
  • Overhead для simple cases.
  • Время на event storming, modeling sessions.
  • Команда должна понимать principles (иначе деградация).

⚠️ 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, обучение.


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.


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.


  1. Event Storming session. Возьми домен (пиццерия, библиотека, банк). С коллегами (или один) разложи events на доске. Найди bounded contexts.

  2. Context map for e-commerce. Опиши контексты: Catalog, Order, Payment, Shipping, Customer. Какие patterns используются (ACL, Customer/Supplier, etc.)?

  3. Rich domain model. Реализуй Order aggregate в Go. State machine (Draft → Placed → Paid → Shipped → Delivered). Invariants. Constructor validation.

  4. Repository pattern. Define interface в domain, реализуй в postgres. Map aggregate ↔ rows.

  5. Domain events. Aggregate генерирует events. Application service вытягивает и публикует через outbox.

  6. ACL для internal API. Symuluj два сервиса с разной моделью (наш Customer и SAP customer). ACL переводит туда-сюда.

  7. Anti-corruption layer. Symulируй интеграцию с Stripe. Наш Domain Money vs Stripe API. ACL.

  8. Subdomain classification. Возьми реальный проект. Classify all features as core/supporting/generic. Где investment overspends на generic?

  9. DDD vs anemic. Refactor anemic order to rich. Покажи разницу в bugs (state machine).

  10. Bounded contexts vs microservices. Покажи в коде: monolith с 3 bounded contexts через modules. Extract один в microservice. Что меняется?


  1. Eric Evans. “Domain-Driven Design: Tackling Complexity in the Heart of Software.” Addison-Wesley, 2003. — оригинал, must read.
  2. Vaughn Vernon. “Implementing Domain-Driven Design.” Addison-Wesley, 2013. — практическое продолжение.
  3. Vaughn Vernon. “Domain-Driven Design Distilled.” 2016. — сжатая версия.
  4. Eric Evans. “DDD Reference” (free). https://www.domainlanguage.com/ddd/reference/
  5. Alberto Brandolini. “Introducing EventStorming.” https://www.eventstorming.com/
  6. Martin Fowler. “AnemicDomainModel.” https://martinfowler.com/bliki/AnemicDomainModel.html
  7. Martin Fowler. “BoundedContext.” https://martinfowler.com/bliki/BoundedContext.html
  8. Nick Tune. “Strategic DDD” articles. https://medium.com/@ntcoding
  9. Scott Millett, Nick Tune. “Patterns, Principles, and Practices of DDD.” Wrox, 2015.
  10. Domain Language website. https://www.domainlanguage.com/
  11. Microsoft. ”.NET Microservices Architecture for Containerized .NET Applications.” (eShopOnContainers reference).
  12. Stefan Hofer. “Domain Storytelling.” 2021.