← Back to blog

Domain-Driven Design in the Real World

·
architectureengineering

Domain-Driven Design is one of those concepts that sounds great in a conference talk and then gets messy the moment you try to apply it to a real codebase with real deadlines and real legacy constraints. I know, because I've been through that mess multiple times.

But after years of applying DDD principles to healthcare and financial systems, I'm convinced it's one of the most valuable tools in an architect's toolkit — when you apply it pragmatically.

Start With the Language

The single most impactful thing about DDD isn't bounded contexts or aggregate roots. It's ubiquitous language.

When I joined a healthcare organization and started working on financial platforms, the biggest source of bugs wasn't bad code — it was misunderstanding. Engineers called things "accounts" when the business called them "shares." What engineering called a "transaction" was actually three different concepts in the financial domain.

The first thing I did was sit down with domain experts and build a shared glossary. It felt tedious. It was transformative. Once everyone used the same words for the same concepts, an entire category of bugs disappeared.

Bounded Contexts Save You From Yourself

The second most valuable DDD concept is bounded contexts. The idea is simple: different parts of your system can have different models of the same real-world concept, and that's not just okay — it's correct.

In our system, a "member" in the enrollment context has different attributes and behaviors than a "member" in the financial context. Trying to create one unified Member model would have been a disaster — it would have been a god object that every team depends on and nobody owns.

Instead, each bounded context has its own model, its own database schema, and its own service. They communicate through events. A member enrolling publishes an event that the financial context consumes and translates into its own model.

Aggregates: Keep Them Small

The biggest mistake I made early on was creating aggregates that were too large. I'd model an entire "Account" with all its transactions, balances, holds, and history as a single aggregate. It was conceptually clean but operationally terrible — concurrency conflicts, slow loads, complex persistence.

The lesson: aggregates should be as small as possible while still enforcing their invariants. An account balance might be an aggregate. An individual transaction is a separate aggregate that references the account. The invariant "account balance can't go negative" is enforced at the balance aggregate level, not by loading the entire account history.

The Pragmatic Approach

Pure DDD can be paralyzing. You can spend weeks debating bounded context boundaries before writing a line of code. Here's my pragmatic approach:

  1. Start with the language. Get the words right before you get the code right.
  2. Draw boundaries around teams, not concepts. Conway's Law is real. Your bounded contexts should align with team ownership.
  3. Don't model everything. Some parts of your system are just CRUD. That's fine. Save DDD for the complex, high-value domains.
  4. Evolve, don't plan. Your first bounded context boundaries will be wrong. Design for change.

Teaching DDD to Teams

As a mentor and leader, I've introduced DDD to multiple teams. The pattern I've seen: engineers who come from a database-first or framework-first mindset struggle initially because DDD asks you to think about behavior before structure.

The approach that works best is pairing DDD concepts with concrete examples from your own codebase. Don't explain aggregates in the abstract — show them the actual entity in your system that should be an aggregate and why. Make it tangible.

DDD isn't a silver bullet. But in domains with real complexity — healthcare regulations, financial compliance, multi-party workflows — it's the difference between a system that grows gracefully and one that becomes unmaintainable.