Domain-Driven Design (DDD): Bridging the Gap Between Code and Business
1. The Language Barrier
Imagine you are building an e-commerce system.
Business Expert says: "When a customer places an order, we need to reserve stock."
Developer writes:
db.insert('orders', { ... });
db.update('products', { stock: stock - 1 }, where: ...);
The code works, but it doesn't speak the same language. "Places an order" became "Insert into DB". "Reserve stock" became "Update column".
As the system grows, this translation gap widens. Business logic gets scattered across controllers, services, and utils. When requirements change, developers struggle to find where the logic lives.
"Where is the logic for bulk discount?" -> "Oh, that's inside the OrderController line 450, mixed with the JSON parsing logic."
Domain-Driven Design (DDD) is a philosophy to align your software architecture with the business domain. It's about making the code expressive.
2. Strategic Design: The Big Picture
Before writing a single class, you need to understand the battlefield. The biggest mistake developers make is trying to model everything at once.
Ubiquitous Language (The Dictionary)
Everyone—developers, domain experts, testers, PMs—must use the same vocabulary.
If the expert calls it a "Flash Sale", the code should have FlashSale class, not DiscountEvent or TimeLimitedPromotion.
This language should appear in:
- Conversations during meetings
- Documentation and Specs
- Variable and Class names in Code
- Database schemas (ideally)
Bounded Contexts (The Map)
A standard problem in large systems is trying to make a Unified Model.
What is a User?
- To the Auth Team, a User is an email and password hash.
- To the Sales Team, a User is a lead with a phone number, budget, and contact history.
- To the Shipping Team, a User is a Recipient with a destination address and zip code.
If you try to make one User class to satisfy everyone, you get a "God Class" with 200 fields, half of which are null.
DDD says: Split it up. Define explicit boundaries (Bounded Contexts).
- Identity Context:
Account (email, pw)
- CRM Context:
Lead (budget, phone)
- Shipping Context:
Recipient (address)
This is the strategic basis for Microservices. Each microservice often maps to one Bounded Context.
3. Tactical Design: The Building Blocks
Once you are inside a context, how do you model the code?
Entities vs Value Objects
- Entity: Defined by Identity. If you change the name of a person, they are still the same person. They have a lifecycle (Create -> Update -> Delete). (e.g., User, Order).
- Value Object (VO): Defined by Value. If you change the red value of a color, it's a new color. They are immutable. (e.g., Color, coordinate, Money).
- Tip: Use VOs as much as possible. They are safer and easier to test because they have no side effects.
Aggregates
An Aggregate is a cluster of objects that are treated as a single unit for data changes.
Example: Car (Root) has Wheels and Engine.
- Can you have a
Wheel floating around without a Car? No.
- Can you save a
Wheel directly to the DB? No, you save the Car.
- The
Car is the Aggregate Root. It controls access to its internal parts.
Rule: External objects can only hold references to the Root. You cannot skip the Root and grab a Wheel directly. This ensures the Root can enforce invariants (e.g., "A car must have exactly 4 wheels").
4. Repositories, Factories, and Services
Repository (The collection interface)
It's an abstraction of your data storage.
In DDD, a Repository behaves like an in-memory collection.
orderRepository.add(order)
orderRepository.findBy(id)
It hides the details of SQL, MongoDB, or external APIs. It speaks the Domain Language, ensuring you deal with Aggregate Roots, not raw data rows.
Factory (The assembler)
Complex objects are hard to create. If creating an Order involves checking stock, validating user status, and calculating taxes, don't put that complexity in the constructor. Use a Factory.
OrderFactory.createOrder(user, items) encapsulates the complexity of creation. It ensures that an object is valid before it exists.
Domain Service (The stateless operator)
Sometimes logic doesn't fit into an Entity.
"Calculating the total interest for a loan over 10 years involving 3 different currency rates."
This belongs in a Service. It is stateless and performs an operation.
InterestCalculationService.calculate(loan, rates)
5. Event Storming: How to Find Your Domain
How do you discover these aggregates and boundaries?
Try Event Storming. It's a workshop where you gather everyone in a room with sticky notes.
- Events: Write down everything that happens in the business in past tense (Orange stickies). "Order Placed", "Item Shipped", "Payment Failed".
- Timeline: Arrange them in chronological order.
- Commands: What triggers the event? (Blue stickies). "Place Order", "Ship Item".
- Aggregates: What data is changed? (Yellow stickies). "Order", "Inventory".
This visualization quickly reveals the complexity of your domain and helps identify Bounded Contexts naturally. It's much more effective than staring at UML diagrams.
6. Do I Need DDD?
DDD is expensive. It requires lots of meetings, deep thinking, and verbose code constraints.
Use DDD when:
- The Domain is Complex: Finance, Logistics, Healthcare, Insurance. The rules are tangled, strict, and change often.
- The Team is Large: You need clear boundaries (Contexts) so teams don't step on each other's toes.
- Long-term Project: You expect the system to live for 5+ years and evolve.
Don't use DDD when:
- Simple CRUD App: A blog, a todo list, a simple admin panel. Just use Active Record or simple services. It's faster.
- Startup MVP: Speed is more important than architectural purity. You might pivot next month anyway.
- Technical Domain: Image processing util, low-level driver. These are usually algorithm-heavy, not business-rule-heavy.
7. DDD and Microservices
You often hear "You need DDD for Microservices." Why?
Because the criteria for splitting a microservice is usually the Bounded Context.
The Modular Monolith
You don't need to start with Microservices. You can apply DDD in a Monolith.
By enforcing strict boundaries between Packages or Modules (e.g., Inventory Module cannot directly import Billing Module classes), you achieve "Modular Monolith".
This is the recommended path. Start with a Modular Monolith to discover the natural boundaries of your domain independently of infrastructure complexity. Once a specific module faces scale issues, split it into a Microservice.
Prematurely splitting services (Distributed Big Ball of Mud) is the #1 reason why microservice projects fail.
8. Conclusion
DDD is not about the patterns (Repository, Entity, Service). It's about the mindset.
It challenges you to stop being a "Coder" and start being a "Problem Solver".
Instead of asking "How do I store this string?", ask "What does this string represent in the real world?".
When your code models the real world accurately, it becomes resilient to change. That is the promise of Domain-Driven Design.