For complex business software, the hardest part is modeling the domain itself. DDD gives you a language and building blocks to align the code with the business.
Why: in complex domains, misunderstanding the business is the biggest risk — DDD insists on a ubiquitous language (the same terms in conversation and in code) and bounded contexts (explicit boundaries where a model and its language apply) so "customer" or "order" means one precise thing within each context. When: use DDD when the domain is genuinely complex; skip it for simple CRUD. Where: bounded contexts are also natural service boundaries, tying DDD to microservice design.
UBIQUITOUS LANGUAGE the SAME words in the business, the model, and the code.
no translation layer between "what they said" and "what
the class is named."
BOUNDED CONTEXT an explicit boundary where a model + its language apply.
Sales context: "Customer" = a lead with a pipeline stage
Billing context: "Customer" = an account with a payment method
Same word, different models — kept separate on purpose.
Bounded contexts make great SERVICE boundaries (see the Architecture course).Why: DDD gives concrete building blocks — entities (identity that persists over time), value objects (defined by their attributes, immutable), and aggregates (a cluster of objects with one root that guards its invariants) — so the domain model enforces its own rules. When: model money and dates as value objects, things-with-identity as entities, and consistency boundaries as aggregates. Where: the aggregate root is the only entry point for changes, which keeps the model always valid.
// VALUE OBJECT — no identity, immutable, defined by its value:
class Money {
constructor(readonly cents: number, readonly currency: string) {}
add(other: Money) { return new Money(this.cents + other.cents, this.currency) }
}
// ENTITY — has identity that persists even as attributes change:
class Order {
constructor(readonly id: string, private lines: Line[] = []) {}
// AGGREGATE ROOT — the only way to change the order; guards its invariants:
addLine(line: Line) {
if (this.lines.length >= 100) throw new Error('order too large')
this.lines.push(line)
}
}Why: DDD pushes the rich behavior into the domain model itself (rules live on entities and aggregates) rather than in anemic data classes with logic scattered in services — so the model expresses the business, not just its storage shape. When: put invariants and operations where the data lives. Where: this is the opposite of the "anemic domain model" anti-pattern and is what makes complex business logic maintainable.
ANEMIC (anti-pattern): data classes with getters/setters; all the logic
lives in "service" classes far from the data.
order.setStatus('SHIPPED') // any caller can set any status
RICH (DDD): behavior lives WITH the data; the model enforces its own rules.
order.ship() // validates state, records the event
// invalid transitions are impossible — the object protects itself
Model the BUSINESS. The database is a detail behind a repository.