PushBackLog

Domain-Driven Design (DDD)

Advisory enforcement Complete by PushBackLog team
Topic: architecture Topic: design Skillset: backend Skillset: fullstack Technology: generic Stage: planning Stage: execution Stage: review

Domain-Driven Design (DDD)

Status: Complete
Category: Architecture
Default enforcement: Advisory
Author: PushBackLog team


Tags

  • Topic: architecture, design
  • Skillset: backend, fullstack
  • Technology: generic
  • Stage: planning, execution, review

Summary

Domain-Driven Design is an approach to software development that centres design on a rich, shared model of the problem domain — co-created by domain experts and engineers. Its core contributions are a set of strategic patterns (bounded contexts, ubiquitous language) and tactical building blocks (aggregates, entities, domain events) that make the implicit structure of complex business domains explicit in code. DDD pays off in domains with genuine complexity and rich business rules; it is overhead in simple CRUD systems.


Rationale

Complexity lives in the domain

The hardest part of most software systems is not the technology — it is understanding the problem domain deeply enough to model it correctly. A system built on an accurate domain model is easier to extend, test, and reason about than one that is technically sophisticated but built on a shallow or incorrect understanding of the business.

DDD places the domain model at the centre of design. Business logic should live in domain objects — not in controllers, not in utility services, not in the database schema — and those objects should use the same language domain experts use. When the code vocabulary drifts from the business vocabulary, the drift signals that the model has become incorrect.

Ubiquitous language closes the translation gap

In most software teams, domain experts and engineers operate in different vocabularies. Domain experts say “subscription renewal” and engineers implement a billing_cycle_trigger with a user_plan_status flag. Every translation introduces ambiguity, and every ambiguity eventually produces a bug or a feature that solves the wrong problem.

DDD’s ubiquitous language eliminates this: one vocabulary, used consistently by domain experts, engineers, product teams, acceptance criteria, and the codebase. If the codebase uses Subscription, Invoice, and Cancellation, those same words appear in every conversation about the domain. The language is owned jointly and updated jointly.

Bounded contexts prevent the big ball of mud

Large systems have multiple sub-domains, each with its own vocabulary and rules. “Customer” means something different in billing than in fulfilment than in support. Forcing a single model to serve all sub-domains produces a model that is technically correct for none.

Bounded contexts are explicit boundaries within which a single model applies. Each context has its own language, its own data model, and ideally its own team ownership. Integration between contexts is modelled explicitly — not through shared database tables or shared class hierarchies. Bounded contexts map naturally to microservices, but they are a design concept that applies equally inside a monolith.


Guidance

Core building blocks

ConceptDefinitionExample
EntityObject with continuous identity across state changesOrder with a persistent order ID
Value ObjectObject defined entirely by its attributes; no identityMoney(100, "GBP")
AggregateCluster of entities/value objects with a single consistency boundaryOrder containing OrderLine list
Aggregate RootThe only entry point for external interaction with an aggregateOrder — never access OrderLine directly
RepositoryAbstraction for retrieving and persisting aggregatesOrderRepository.findById()
Domain ServiceStateless domain logic that spans multiple entitiesPricingService.calculateDiscount()
Domain EventAn immutable record that something significant occurred in the domainOrderPlaced, PaymentFailed
Bounded ContextAn explicit boundary within which a model is consistentBilling, Fulfilment, Support

Aggregate design rules

  1. Reference other aggregates by identity onlyOrder holds a customerId string, not a Customer object
  2. Enforce invariants at the aggregate root — all consistency rules for the aggregate are upheld by the root itself
  3. Keep aggregates small — large aggregates create lock contention and slow transactions
  4. One transaction per aggregate — if you need to change two aggregates in one transaction, the boundary is probably wrong
// WRONG — bypassing the aggregate root
const item = orderRepository.findItemById(itemId);
item.cancel();
await orderItemRepository.save(item);

// CORRECT — operating through the aggregate root, which enforces invariants
const order = orderRepository.findById(orderId);
order.cancelItem(itemId); // Order validates that cancellation is allowed in current state
await orderRepository.save(order);

Domain events for aggregate coordination

class Order {
  private domainEvents: DomainEvent[] = [];

  place(customer: Customer, items: OrderItem[]): void {
    this.validateCanPlace();
    this.status = OrderStatus.Placed;
    this.domainEvents.push(new OrderPlaced(this.id, customer.id, this.total));
  }

  pullDomainEvents(): DomainEvent[] {
    const events = [...this.domainEvents];
    this.domainEvents = [];
    return events;
  }
}

Strategic patterns for multi-context systems

PatternWhen to use
Shared KernelTwo contexts genuinely share a small sub-model; both teams maintain it jointly
Customer/SupplierDownstream context depends on upstream; upstream respects downstream’s reasonable needs
Anti-Corruption LayerTranslating between your clean domain model and a messy legacy or external system
Open Host ServiceUpstream publishes a stable protocol for multiple downstream consumers
Separate WaysContexts are fully independent; no integration needed

When DDD is and isn’t appropriate

SituationRecommendation
Complex domain with rich business rules and expert-dense knowledgeApply DDD — the investment pays off
Multiple teams working on distinctly different sub-domainsApply bounded contexts at minimum
CRUD system with no business rules beyond basic validationSkip DDD — it adds overhead for no modelling benefit
Short-lived prototype or spikeSkip DDD — optimise for speed
Legacy system modernisationStart with an anti-corruption layer; introduce tactical patterns incrementally

Further reading

  • Domain-Driven Design — Eric Evans (the foundational text)
  • Implementing Domain-Driven Design — Vaughn Vernon (practical guide)
  • Domain-Driven Design Distilled — Vaughn Vernon (concise introduction)