design·lab

Design approach

Domain-Driven Design

When the business domain is genuinely complex, model the software around it — using the words the experts already use — instead of bending the domain to fit a database schema.

✗ The problem

A big ball of mud, and everyone speaks a different language

Business rules leak into controllers and SQL scripts. The model itself is just data — getters and setters, no behavior. And "cart", "order", "purchase" all mean the same thing to three different people.

// rule buried in a controller, not the domain
class OrderController {
  checkout(req) {
    if (req.total > 1000) req.discount = 0.1;
    db.update('orders', req.id, { total: req.total });
  }
}
// the "model" is anemic — just fields
class Order { getTotal(){} setTotal(t){} }
Devs say "cart", Sales says "order", Billing says "invoice" — same concept, three names, zero shared understanding. Logic ends up wherever it was easiest to paste it.
✓ The approach

Model the domain, speak the experts' language

Code and domain experts share one Ubiquitous Language. Complex domains split into Bounded Contexts; each has its own valid model.

class Order {  // Entity — has identity
  addLineItem(sku, qty, price) { /* enforces rules */ }
  confirm() { /* locks the order */ }
}
class Money {  // Value Object — equal by value
  constructor(amount, ccy) { ... }
}
Sales
bounded context
Shipping
bounded context
Billing
bounded context
Order
aggregate root
↓ guards invariants
Line item
Line item
✓ See it live

The aggregate root enforces its own rules

The Order aggregate — not a controller — decides what's allowed: total capped at $1000, and no edits once confirmed.

Order aggregate root OPEN
Items:
Total: $0 / $1000 limit

Accepted / rejected log

    ✓ Takeaway

    Model the domain, not the database

    • Ubiquitous Language: code and domain experts use the exact same words.
    • Model the domain, not a table — behavior lives on entities, not controllers.
    • Aggregates guard invariants: the root is the only door in; it rejects invalid changes.
    • Split by Bounded Context: each context gets its own model — no single "god" schema.
    • Caution: DDD is heavyweight. Reserve it for complex core domains — plain CRUD doesn't need it (YAGNI).
    🎯 Relates: DDD's boundaries drive CQRS & Event Sourcing; aggregates lean on Factories and strong SRP boundaries.