Backend6 min read

Domain-Driven Design

Apply DDD to complex business domains - bounded contexts, aggregates, value objects, domain events, ubiquitous language, and anti-corruption layers.

DDDDomain-Driven DesignBounded ContextAggregateValue Object

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

  1. 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.
  2. 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.
  3. 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.
  4. Publish domain eventsDomain events capture "something important happened in the business domain" - enabling other contexts to react without tight coupling.
  5. 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 contexts

Examples

Input

Output

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/

More skills in this library

Browse all technical skills - Mobile, AI/ML, Web, Backend, Design, and Product.