Domain-Driven Design
Apply DDD to complex business domains - bounded contexts, aggregates, value objects, domain events, ubiquitous language, and anti-corruption layers.
skill: Domain-Driven Design category: Backend tags: [DDD, Domain-Driven Design, Bounded Context, Aggregate, Value Object] readingTime: 6
DDD is most valuable for complex business domains - when the rules are intricate and change frequently. For CRUD apps, DDD is over-engineering. The core insight: the domain model should speak the language of the business, not the language of the database. Code changes should flow from business concept changes.
Workflow
- Establish ubiquitous languageUbiquitous language eliminates translation errors between business and technical teams. Code variable names, method names, and class names should match the terms the domain experts use.
- Design aggregates to protect invariantsAggregates are clusters of objects treated as a single unit for data changes. They enforce business rules (invariants) that span multiple objects.
- Use value objects for domain conceptsPrimitives (strings, numbers) don't capture domain rules. A Money value object enforces currency rules; an Email object enforces format. Value objects are immutable and compared by value.
- Publish domain eventsDomain events capture "something important happened in the business domain" - enabling other contexts to react without tight coupling.
- Define bounded contexts and anti-corruption layersDifferent teams use different models of the same concept. An anti-corruption layer translates between models, preventing one context's concepts from polluting another.
Output Format
DDD: ubiquitous language in code, aggregates enforcing invariants with domain events, immutable value objects, outbox pattern for event publishing, and anti-corruption layers between bounded contexts.
Aggregate: class Order { addItem(); place() - publishes OrderPlaced event }
Value Object: class Money { static of(); add(); equals() - immutable }
Domain Event: new OrderPlaced({ orderId, userId, items, totalCents })
Outbox: save(aggregate) + INSERT INTO outbox in same transaction
ACL: CatalogProductAdapter.toOrderLineItem() translates between contextsExamples
Edge Cases & Gotchas
- Aggregate size - Large aggregates (Order with 500 items) load slowly and create lock contention. Keep aggregates small - only include objects that must change together for invariant consistency.
- Domain events vs. integration events - Domain events are rich, internal objects with business meaning. Integration events are slimmed-down, stable contracts published to other bounded contexts. Don't publish internal domain events directly.
- Repository vs. DAO - A repository returns fully hydrated aggregates (all invariants enforced). A DAO (Data Access Object) returns raw data. Mix them up and your aggregate's invariants are bypassed.
- Event sourcing and DDD - Event sourcing stores domain events as the primary source of truth (no "current state" table). It pairs naturally with DDD aggregates but adds significant complexity - only use it when event history is a core requirement.
When to Break These Rules
- Apply tactical DDD patterns (aggregates, value objects) inside a service without requiring strategic DDD (bounded contexts, context maps) when team size doesn't warrant it.
- Use a simple transaction script pattern (service reads, applies rules, writes) for simple CRUD operations - DDD aggregates add indirection without benefit when there are no complex invariants.
Reference Files
https://www.domainlanguage.com/ddd/reference/https://ddd-practitioners.com/