Domain-Driven Design (DDD): Writing Software That Matches Reality
Most software projects fail not because the code is bad, but because the code solves the wrong problem.
We've all seen "Anemic Domain Models." Classes that look like this:
javascriptclass Order { public id: string; public status: string; public total: number; }
This is not an Order. This is a database row wearing a tuxedo.
Real business logic is messy. "You cannot cancel an order if it has already shipped, unless the user is a VIP."
In an Anemic model, this logic lives in a OrderService 500 lines away. In DDD, it lives in the Order.
In this guide, we explore how to put the logic back where it belongs.
1. The Ubiquitous Language
If the Domain Expert says "A User places an Order," but your code says db.insert('orders', ...), you have a translation error.
The Golden Rule: The code should speak the same language as the business.
- Business: "We need to onboard a client."
- Bad Code:
createClient(data). - Good Code:
client.onboard().
2. Bounded Contexts (The Secret Sauce)
A "Product" means something different to Sales than to Shipping.
- Sales Context: Product = { Name, Price, Description, SEO Terms }.
- Shipping Context: Product = { Weight, Dimensions, Hazardous Material Flag }.
Attempting to build one giant Product class that satisfies both departments is a disaster.
DDD Solution: Create two contexts.
Sales.ProductShipping.Product
They share an ID, but nothing else.
3. Rich Domain Models vs. Anemic Models
Stop writing "Getters and Setters." Start writing Behavior.
The Anemic Way (Bad):
javascript// Service Layer function shipOrder(orderId) { const order = db.getOrder(orderId); if (order.status === 'PAID') { order.status = 'SHIPPED'; db.save(order); } }
The Rich Way (Good):
javascript// Domain Layer class Order { ship() { if (this.status !== 'PAID') { throw new DomainError("Cannot ship unpaid order."); } this.status = 'SHIPPED'; this.addEvent(new OrderShipped()); } } // Service Layer const order = repo.get(orderId); order.ship(); // The logic is INSIDE the object. repo.save(order);
4. Aggregates and Roots
In a complex web of objects (Order -> LineItems -> Product), you need a boss. The Aggregate Root is the boss.
- Rule: You never access a
LineItemdirectly. You access theOrder. - Why? The
Orderguarantees consistency. It ensures the total price matches the sum of the items.
Summary
DDD is overkill for a To-Do list app. But for complex business software, it is the only way to stay sane.
- Speak the Language: Code should sound like the expert.
- Split the Contexts: Don't make god-classes.
- Encapsulate Logic: Put the rules inside the objects, not in "Services."
