Domain-Driven Design (DDD): Writing Software That Matches Reality

Domain-Driven Design (DDD): Writing Software That Matches Reality

2025-05-10
3 min read

Executive Summary

"Why "Anemic Domain Models" are killing your project. A guide to Ubiquitous Language, Bounded Contexts, and rich logic."

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:

javascript
class 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.Product
  • Shipping.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 LineItem directly. You access the Order.
  • Why? The Order guarantees 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.

  1. Speak the Language: Code should sound like the expert.
  2. Split the Contexts: Don't make god-classes.
  3. Encapsulate Logic: Put the rules inside the objects, not in "Services."
Interactive Practice Sandbox • Zero Risk

Theory is Good. Muscle Memory is Better.

Don't let your first time handling this scenario be in front of your engineering team or manager. Rehearse your points with our interactive AI personas, get real-time feedback on assertiveness and clarity, and calibrate your approach before it counts.


Written by The DevToLead Team

We are a group of senior engineers and tech leads sharing our real-world experience to help you grow. Our mission is to bridge the gap between junior developers and confident technical leaders.

The Tuesday Leadership Dilemma

One High-Stakes Scenario in Your Inbox Every Tuesday

Rehearse the hardest parts of engineering leadership: tense scope negotiations, defensive 1-on-1s, and architectural stalemates. Complete with suggested diplomatic scripts.

100% FreeNo spam everUnsubscribe in 1 click
Or try the Live AI Simulator