What DDD is — and what it is not
Domain-driven design (DDD) is an approach to software development that puts the business domain at the centre — not the framework, not the database, not the UI pattern. Eric Evans coined the approach in 2003 with Domain-Driven Design: Tackling Complexity in the Heart of Software. Two decades on, DDD is less a method set than a frame of thinking that brings together three disciplines.
First: a shared language between business and engineering — Evans calls it the ubiquitous language. Terms that appear in the business appear in the code, verbatim. A "Vertrag" (contract) in the business conversation is a Vertrag in the code, not a ContractRecord or a ContractDTO. It sounds trivial; it isn't. When domain terms shift during the translation into code, they come back — as bugs, misunderstandings and specifications nobody understands any more.
Second: explicit models that capture the domain — not the database. A good model describes what a contract does, which states it can be in and which rules apply. A bad model describes how it is stored.
Third: a structure that separates complexity. Few systems consist of a single domain. Banks have credit risk, payments and complaint management — three domains with their own languages, rules and lifecycles. DDD offers tools to make that separation visible, instead of hiding it.
DDD is not a framework and not a delivery model. It replaces neither Scrum nor microservices, neither TDD nor hexagonal architecture. It is orthogonal: you can work agile with or without DDD, build monolithic or distributed. But anyone who has worked in a codebase without DDD discipline knows the feeling that "something somewhere is off" — and that is exactly where DDD steps in.
Strategic design — where the boundaries run
Strategic DDD answers the question: how do we cut the system as a whole? Three concepts carry the answer.
Bounded context
A bounded context is a defined area in which a model holds consistently. Inside the context, every term has exactly one meaning. Outside, the same term may mean something else — and that is intentional. A "customer" in sales (a lead, a potential person) is a different "customer" than in receivables (a debtor with an open invoice). Both exist legitimately, but they belong in different contexts. Trying to merge them into a single class builds a monster that hurts both sides.
Context map
A context map shows the relationships between bounded contexts. Which context sends data to which? Who is supplier, who is consumer? Which contracts hold? Typical relationship patterns are shared kernel (a shared core model), customer-supplier (the supplier commits to the consumer), conformist (the consumer has to take what comes) and anti-corruption layer (a translation layer that keeps foreign models out).
Subdomains
Three kinds of subdomains help prioritise investment:
- Core domain — the area in which the company differentiates itself. This is where most care is warranted; this is what you build yourself.
- Supporting subdomain — necessary, but not a differentiator. Can be built, can be bought.
- Generic subdomain — everybody has this (authentication, logging, invoicing). Buy or integrate a standard product.
Building your own in the generic subdomain burns money. Buying off the shelf in the core domain gives away your differentiation. This classification is the first step of any DDD strategy — and it should not be made by engineering alone, but jointly with management.
Tactical design — the building blocks of the model
Inside a bounded context, tactical DDD provides the building blocks for the domain model. Six of them are enough to cover the vast majority of cases:
- Entity — an object with identity that persists over time. A
Vertrag (contract) with the number V-2025-0042 is an entity — even if all its fields change, it remains the same contract. - Value object — an object without identity, defined by its values. A
Geldbetrag (money amount) of "120.50 EUR" is a value object; two amounts with identical values are indistinguishable. Value objects are immutable; changing one means producing a new one. - Aggregate — a cluster of entities and value objects treated as a unit, with exactly one aggregate root as the access point. Example:
Bestellung (order) is the root, Bestellpositionen (line items) belong to it, but nobody accesses them directly. The aggregate boundary protects consistency: business rules hold within, between aggregates you work with eventual consistency. - Domain event — something of business significance has happened:
VertragGeschlossen (contract signed), ZahlungEingegangen (payment received), BerechtigungEntzogen (permission revoked). Events are immutable facts; other bounded contexts react to them without needing to know the original context. - Repository — an abstraction for loading and saving aggregates. From the domain model's point of view, a repository is a collection; whether a SQL database, an event store or a REST API sits underneath is infrastructure, not domain.
- Domain service — logic that doesn't naturally belong to a single entity. Example: a
PreisRechner (price calculator) that applies discounts across several contracts. Caveat: domain services are an escape hatch — too many of them push logic out of the entities and lead to an anaemic domain model (see pitfalls).
Building blocks in code
What this looks like in practice is shown by a brief TypeScript example — an immutable value object and an entity with a business method:
class Geldbetrag {
constructor(
readonly betrag: number,
readonly waehrung: 'EUR' | 'USD' | 'CHF'
) {}
add(other: Geldbetrag): Geldbetrag {
if (this.waehrung !== other.waehrung) {
throw new Error('Currencies do not match');
}
return new Geldbetrag(this.betrag + other.betrag, this.waehrung);
}
}
class Vertrag {
constructor(
readonly id: VertragNummer,
private status: VertragStatus
) {}
sign(): void {
if (this.status !== 'draft') {
throw new Error('Only drafts can be signed');
}
this.status = 'active';
}
}The decisive trait shows up in the sign method: the business rule ("only drafts can be signed") sits where the state sits — not in some service class next door. This relocation of behaviour to the entity is what separates a living domain model from a data-structure shell.
How a model emerges
How do you arrive at a model? Two workshop methods have proven themselves in projects and complement each other — the choice depends on the character of the domain.
Event storming
Alberto Brandolini developed event storming as a workshop format. A group of business experts and engineers sticks orange notes with domain events ("order placed", "payment received", "dunning issued") onto a wall in chronological order. Commands, actors, policies and aggregates are added afterwards. The result: a shared picture of the business in two to four hours. The key point: it isn't about the system to be built, but about the business that takes place — regardless of technology.
Domain storytelling
Stefan Hofer and Henning Schwentner coined domain storytelling as an alternative. Business experts tell concrete stories ("when an application comes in, the clerk first checks …") while a facilitator visualises them as pictograms. The method is slower than event storming but a better fit when the business consists of many sequential processes — typical in public administration.
Both methods share a core idea: the model emerges in dialogue, not in solitary reasoning. Anyone who tries DDD without bringing the business to the table ends up with an engineer's model — which the first workshop with the business team has to rewrite.
Practical noteThe most common mistake in DDD workshops is to talk about technology too early. The moment terms like "service", "microservice" or "database" appear, the conversation slides from the domain into architecture. Keep the first workshop technology-free — all it needs is pens, sticky notes and a blank wall.
Pitfalls in practice
Four anti-patterns appear especially often in DDD projects — and they are the most common source of disappointment with the method.
- Anaemic domain model. Entities without behaviour — just data structures whose logic lives in services. The result: procedural code in object-oriented clothes. The remedy: if a business rule applies to an entity, it belongs there as a method. A contract that cannot sign itself is not a contract — it is a table.
- Distributed monolith. Bounded contexts implemented as microservices, but coupled synchronously via REST. Every service calls every other. The result: all the downsides of distributed systems, none of the upsides. The remedy: between contexts, go asynchronous via events. Synchronous calls only as a deliberate exception, not the default.
- Premature DDD. A prototype or a three-table CRUD project gets the full DDD treatment — aggregates, domain events, CQRS. The result: three weeks of architecture, one week of features. The remedy: DDD pays off when domain complexity exists. For trivial apps, a lean layered approach is enough.
- Technical-only DDD. The team knows the tactical patterns (entity, value object, repository), but nobody talks to the business. The result: a tidy model that describes the business incorrectly. The remedy: ubiquitous language is not optional — it is the entry ticket to the method.
When DDD pays off — and what that means for low-code
DDD doesn't pay off for every use case. An honest assessment — and a note on why the connection to low-code platforms tends to be underestimated.
Clearly worthwhile:
- Complex business domains with many rules, special cases and long-lived workflows — typical in banking, insurance, energy and public administration.
- Systems that evolve over years and where domain knowledge is the most important investment — software that outlasts multiple generations of engineers.
- Platforms with multiple teams that should work independently. Bounded contexts give each team a clear area of ownership and noticeably reduce coordination overhead.
Hardly worthwhile:
- Simple CRUD applications — if 80 % of the time is spent writing forms over tables, you don't need aggregates.
- Throwaway prototypes. DDD is an investment; on a two-week project, it doesn't pay back.
- Technical tooling without a business angle (build tools, linters, monitoring dashboards). Here the "domain" is technical, not business.
In the low-code context: DDD and low-code fit together better than first impressions suggest. A mature low-code platform speaks the language of the business — that is, the ubiquitous language. Anyone building such a platform is, knowingly or not, modelling entities, value objects and aggregates; they put exactly those building blocks into the users' hands, just without naming them. The reverse holds too: a low-code platform that doesn't structure its own architecture along bounded contexts becomes unmaintainable past mid-size — modules blur, the same terms suddenly mean different things, and every release turns into a guessing game. We therefore treat DDD in our own low-code solutions not as optional theory but as a construction principle — visible in the module layout, the modelling UI and the way domain events flow between modules.
RecommendationWe rarely start DDD projects with the full tactical repertoire. The first workshop goes into event storming or domain storytelling — the output is a language, a model and a rough context map. On that basis we decide which aggregates are worth the effort, which bounded contexts need which level of detail, and where a lean CRUD service is more honest than an aggregate construction. DDD works best when applied selectively — and precisely where the complexity warrants it.