Domain-Driven Design has a reputation problem. Half the industry thinks it’s an academic pattern catalogue — entities, repositories, factories, a folder called domain/ — and the other half thinks it’s just “microservices, but with more meetings.” Both miss the point.
Here’s the honest version: DDD is mostly about drawing boundaries and agreeing on language. The famous tactical patterns are the small half. The valuable half is strategic — figuring out what your business actually does, where one part ends and another begins, and where it’s worth investing real modeling effort. This guide walks through both halves in plain language, shows how they connect, and — just as importantly — tells you when not to bother.

Where DDD Actually Came From
Domain-Driven Design was named by Eric Evans in his 2003 book Domain-Driven Design: Tackling Complexity in the Heart of Software. The core claim is simple and still radical in a lot of teams: the hardest part of most software isn’t the technology — it’s the domain. The tangled business rules of insurance, logistics, healthcare, or trading are what actually make a system hard, and the best way to tame that complexity is to build a shared model of the domain and let it drive the design.
That’s it. Everything else in DDD is machinery in service of that one idea.
The Heart of It: Model and Ubiquitous Language
Two concepts sit at the centre of DDD, and if you only take two things away, take these.
The model is a deliberate, simplified picture of the domain — the concepts, rules, and relationships that matter, and nothing that doesn’t. It’s not the database schema and it’s not the class diagram; it’s the shared mental model the whole team reasons with.
The ubiquitous language is the vocabulary of that model — and the rule is that everyone uses it everywhere. Domain experts, product people, developers, tests, and the code itself all use the same words for the same things. If the business says “a policy is lapsed,” then there’s a lapsed state in the code, not status = 3. When the language in conversations matches the language in the codebase, a whole category of translation errors and misunderstandings simply disappears.
This is the cheapest, highest-leverage practice in all of DDD, and it needs no framework. You can start tomorrow: listen to how the domain experts actually talk, write those words down, and refuse to invent parallel developer-only jargon.
Strategic DDD: The Half That Matters Most
Strategic design is about the big picture — how you carve up a large domain into parts that each make sense on their own. This is where DDD earns its keep, and it’s the part teams most often skip.
Bounded Contexts
A bounded context is an explicit boundary within which one model and one language are consistent. The crucial realization is that the same word means different things in different parts of the business.
Take “Customer”:
- In Sales, a Customer is a lead with a pipeline stage, a probability, and an account owner.
- In Billing, a Customer is an account with payment terms, invoices, and a credit limit.
- In Support, a Customer is a person with a contact history and open tickets.
The instinct of many teams is to build one giant Customer class that serves all three. That object becomes a bloated mess that no one owns and everyone is afraid to change. DDD says the opposite: let each context have its own Customer model, tuned to its own needs, and define explicitly how they relate. Each bounded context is small, coherent, and owned by a team that shares one language.

Subdomains: Where to Spend Your Best People
Not every part of your system deserves the same investment. DDD splits the problem space into three kinds of subdomain:
- Core domain — what actually makes your business different and hard to copy. This is where your best engineers, your deepest modelling, and your full DDD effort should go. For a logistics company, it’s routing and scheduling; for a bank, it’s risk and settlement.
- Supporting subdomain — necessary for the business but not a differentiator. It needs to exist and work, but it doesn’t need to be brilliant. Build it simply.
- Generic subdomain — solved problems that every company has: authentication, notifications, payments, PDF generation. Don’t model these — buy them or use an off-the-shelf service. Spending your modelling energy here is pure waste.
The single most valuable strategic question in DDD is: what is our core domain? Everything else follows from getting that answer right.
Context Mapping: How the Pieces Relate
Once you have multiple bounded contexts, you need to describe how they connect — technically and organizationally. This is a context map, and DDD gives it a vocabulary:
- Partnership — two contexts (and their teams) succeed or fail together and coordinate closely.
- Customer–Supplier — a downstream context depends on an upstream one, and the upstream team factors the downstream’s needs into its plans.
- Conformist — the downstream simply accepts the upstream’s model as-is (common when the upstream is a vendor or another team that won’t bend).
- Anticorruption Layer (ACL) — the downstream builds a translation layer that converts the upstream’s model into its own, so a messy or foreign model doesn’t leak in and corrupt the local one. This is one of the most useful patterns in practice, especially when integrating with legacy systems.
- Open Host Service / Published Language — an upstream context offers a well-defined, documented interface (often an API with a shared schema) so many downstreams can integrate the same way.
You don’t need to memorise the catalogue. The point is that the relationships between contexts are a design decision, not an accident — and drawing them explicitly saves you from the classic “big ball of mud” where everything depends on everything.

Tactical DDD: The Building Blocks
Tactical design is the toolkit for expressing your model in code inside a bounded context. These are the patterns everyone remembers — but remember they’re the smaller half, and they only matter once you’ve drawn good boundaries.
- Entity — an object defined by its identity, not its attributes. A
Userwith ID 42 is the same user even if their name and email change. Two entities are equal when their IDs match. - Value Object — an object defined entirely by its values, with no identity, and ideally immutable.
Money(10, "EUR"), aDateRange, anAddress. Two value objects are equal when their values are equal. Preferring value objects makes code safer and clearer — aMoneytype can’t be accidentally added to aDistance. - Aggregate — a cluster of entities and value objects that change together and must stay consistent as a unit. One entity is the aggregate root: the only object outside code is allowed to reference, and the guardian of the aggregate’s invariants (the rules that must always hold). An
Orderaggregate might containOrderLinevalue objects and enforce “an order total can never be negative.” - Domain Event — a record that something meaningful happened in the domain:
OrderPlaced,PaymentReceived,PolicyLapsed. Events are how aggregates and contexts stay in sync without tight coupling, and they map cleanly onto an event-driven backbone. - Repository — a collection-like abstraction for loading and saving aggregates, hiding the persistence details. You ask a repository for an aggregate by identity; you don’t write SQL in your domain code.
- Domain Service — a home for domain operations that don’t naturally belong to a single entity or value object, like a transfer between two accounts.
Designing Good Aggregates
Aggregates are the tactical pattern people get most wrong, so it’s worth being precise. The widely-taught rules (sharpened by Vaughn Vernon’s Effective Aggregate Design) are:
- Keep aggregates small. A big aggregate means big transactions and lots of contention. Prefer many small aggregates over one giant one.
- Protect invariants through the root. All changes go through the aggregate root so it can enforce the rules. Nothing reaches inside and mutates a child directly.
- Reference other aggregates by identity, not by object. An
Orderholds aCustomerId, not a wholeCustomerobject. This keeps aggregates independent and loadable on their own. - One aggregate per transaction. A single transaction should modify exactly one aggregate. When another aggregate needs to react, publish a domain event and update it in a separate transaction — accepting eventual consistency.
That last rule is the bridge between tactical DDD and distributed systems: consistency inside an aggregate is immediate; consistency between aggregates is eventual, carried by events.

DDD and Microservices
Because a bounded context is a coherent, self-contained model with its own language and data, it’s usually the right size for a microservice. That’s the real link between DDD and microservices: DDD gives you a principled way to decide where to draw service boundaries, instead of splitting by technical layer or by guesswork.
But the two are not the same thing, and conflating them causes real damage:
- You can do DDD in a modular monolith — one deployable, one module per bounded context, clean boundaries in the code. For most teams this is the pragmatic starting point.
- You can build microservices without DDD — and teams that do often carve services along the wrong lines and end up with a distributed monolith: services so coupled they must deploy together.
The order matters: discover the bounded contexts first, then decide deployment. Split a context into its own service when there’s a concrete reason — independent scaling, team autonomy, fault isolation — not because microservices are fashionable.
When DDD Is Worth It — and When It’s Not
DDD is an investment, and like any investment it has a bad return in the wrong place.
Reach for DDD when:
- The core domain is genuinely complex — rich business rules that are hard to get right and expensive to get wrong (insurance, logistics, trading, healthcare, tax).
- The domain keeps evolving, so a clear model and language pay off repeatedly over years.
- Multiple teams need to work on the same large system without stepping on each other — bounded contexts give them clean seams.
Skip most of it when:
- The app is essentially CRUD — forms over data with little real logic. The full tactical toolkit is pure ceremony here.
- You’re working in a generic or supporting subdomain — buy it, don’t model it.
- The team is small and the product is early, and the domain isn’t understood well enough yet to draw good boundaries.
Note the asymmetry: even in a simple project, the strategic practices — ubiquitous language, knowing your core domain — are cheap and almost always worth it. It’s the heavy tactical machinery that you should apply selectively.
Common Mistakes
A few traps show up again and again:
- Doing only tactical DDD. Teams adopt entities, repositories, and a
domain/folder but never draw bounded contexts or talk to domain experts. This is DDD cosplay — the expensive patterns without the payoff. - One model to rule them all. Refusing to let “Customer” mean different things in different contexts, and building a god-object instead.
- Anemic domain model. Entities that are just bags of getters and setters, with all the logic sitting in “service” classes. The rules belong in the model.
- Aggregates too big. Pulling half the schema into one aggregate, then fighting transaction contention and lock timeouts forever.
- Modelling generic subdomains. Lovingly hand-building an authentication or notification system that you should have bought.
How to Actually Start
You don’t adopt DDD by reading a book and renaming your folders. Start small and strategic:
- Listen for the language. Sit with domain experts and write down the exact words they use. That vocabulary is the seed of your ubiquitous language.
- Run an Event Storming session. This workshop technique (from Alberto Brandolini) gets developers and domain experts around a wall of sticky notes to map the domain’s events, commands, and — crucially — where the natural boundaries fall. It’s the fastest way to discover bounded contexts.
- Name your core domain. Decide where the business actually differentiates, and agree to invest your best modelling effort there and keep the rest simple.
- Draw a rough context map. Even a whiteboard sketch of your contexts and how they relate will expose hidden coupling and bad dependencies.
- Apply tactical patterns only in the core. Introduce aggregates, value objects, and domain events where the complexity justifies them — not everywhere.
The Bottom Line
Domain-Driven Design isn’t a folder structure, and it isn’t a synonym for microservices. It’s a discipline for taming complex domains by modelling them deliberately and drawing honest boundaries. The strategic half — ubiquitous language, bounded contexts, knowing your core domain — is the part that pays off almost universally and costs almost nothing. The tactical half is a sharp toolkit that shines in genuinely complex core domains and adds only ceremony everywhere else.
Use it where the domain is hard. Keep everything else simple. That’s DDD done well — no jargon required.




From the community
Discussion on the Fediverse
Replies from Mastodon and Bluesky — straight from the open web, no tracking.
Loading replies …
No replies yet. Start the conversation:
Replies could not be loaded right now.