Logo von nextlevels
Request a project

Domain-Driven Design in Practice: What DDD Really Delivers for SMEs

When DDD pays off for grown custom software, how to start with Event Storming, the core domain and bounded contexts, and why a modular monolith is usually the better target architecture

Software

Ask three department heads at a mid-sized machine builder what an order is, and you'll get three answers. For sales, it's an accepted offer with price and discount. For production, it's a production order with a bill of materials. For service, it's a repair case on a machine delivered seven years ago. The software knows exactly one table called order, and all three meanings live in it at the same time.

My claim: more software projects fail on this one word than on the choice of framework. Domain-Driven Design is the method that takes exactly this problem seriously. Many people think DDD is architecture theory for corporations with a hundred developers. I think that's a misunderstanding. For SMEs, DDD matters even more, because the business knowledge sits in a few heads and the teams are too small to drag badly cut software along for years.

My rule of thumb: clarify the language and the boundaries first, and use code patterns only where the business is genuinely complicated. One half of the DDD literature decides whether anyone will still understand what the field status2 means ten years from now. You can leave the other half on the shelf for now.

Domain-Driven Design: the database table order, pulled in three directions by sales, production and service
Domain-Driven Design: the database table order, pulled in three directions by sales, production and service

What is Domain-Driven Design?

In 2003, Eric Evans wrote an entire book about exactly this table, even if he calls it something else: "Domain-Driven Design: Tackling Complexity in the Heart of Software". His thesis: the hardest complexity in enterprise software lies in the business domain, meaning the rules, exceptions and dependencies of the business. If you model this domain sloppily in code, you build software in which every new requirement costs more than the last.

DDD provides two toolboxes for this. Strategic design answers how to split a large system into sensible areas and how those areas talk to each other. Tactical design provides patterns for the code within an area: entities, value objects, aggregates, repositories, domain events. Many introductions start with the tactical toolbox, including Evans' book itself. For SMEs, the strategic one delivers more.

Two terms carry the whole concept. The ubiquitous language is the shared business language that the business department and development use identically, in meetings just as in class names. A bounded context is the boundary within which this language applies unambiguously. Outside it, the same word may mean something else, and that is explicitly allowed.

In short: DDD accepts that there is no single unified company model and turns that fact into an architectural rule.

Strategic design: bounded contexts and subdomains

The classic reflex in legacy software is to squeeze every meaning of a term into one shared data model. Every IT lead knows the result: a table with 140 columns, of which each department uses 30, and fields like status2 or delivery_date_new whose meaning nobody can explain any more. Evans explicitly considers full unification of the domain model for a large system neither feasible nor cost-effective, as Martin Fowler summarises in his explanation of the bounded context.

Three terms, three views: how a machine builder's sales, production and service teams use the same words
Term Sales Production Service
Order Accepted offer with price and discount Production order with bill of materials and capacity Repair or maintenance case
Customer Contact person with potential Barely relevant Operator of an installed machine
Item Sellable product with configuration Assembly with routing Spare part with compatibility list

DDD gives each of these areas its own bounded context with its own model. The sales order knows discounts but not routings. The production order knows capacities but not payment terms. Where contexts exchange data, a defined interface translates it into the respective model. If accounting later joins with its view of invoices and debtors, it gets another context, and its fields never show up in production.

Before and after: one giant table becomes three bounded contexts, each with its own model
Before and after: one giant table becomes three bounded contexts, each with its own model

The second strategic step is dividing the domain into subdomains. Evans distinguishes the core domain, where your company stands out from the competition, from generic subdomains that every company needs in the same way. In 2013, Vaughn Vernon established a third category in "Implementing Domain-Driven Design": supporting subdomains, which are specific to you but don't create a competitive advantage.

For a machine builder with a product configurator, the core domain would be the variant logic, meaning the rules for which assemblies fit together. Service scheduling would be supporting; financial accounting and payroll would be generic.

This classification is the real value of DDD in a budget discussion. It shows you where in-house development makes money and where it burns money.

Core domain, supporting and generic subdomains, illustrated with a machine builder
Core domain, supporting and generic subdomains, illustrated with a machine builder

Why DDD delivers more for SMEs than for large corporations

According to Germany's Federal Statistical Office (Destatis), around 13.4 million people in the labour force will reach statutory retirement age by 2039, just under a third of the 2024 labour force. In many mid-sized companies, business rules that are written down nowhere leave with them: the inside sales rep knows which options rule each other out technically, and the dispatcher knows the special rules for key accounts by heart.

DDD forces this knowledge into an explicit language and into code that represents it readably. Knowledge that could retire becomes a term in the glossary and a rule in a place you can find.

At the centre of the system landscape there is almost always an ERP system, around which custom software, Excel macros and interfaces have grown over the years. Without clear boundaries, the ERP's data model seeps into every in-house development.

The same cryptic field names and status codes show up in the customer portal, the service app and the Excel export, and only someone who knows the ERP customising can understand them. A bounded context with its own translation layer concentrates this dependency in exactly one place.

Then there's team size. Many SMEs employ a handful of developers, in-house or at an agency. As early as 1968, Melvin Conway observed that organisations design systems that copy their own communication structures. With five developers and no clear responsibilities, the software becomes as tangled as the arrangements between them.

The result is a familiar picture: the only colleague who understands pricing is on holiday for two weeks, and nobody dares to touch the order table. Clear context boundaries make it possible for one person to own an area without having to keep the whole system in their head.

Destatis: by 2039, around 13.4 million people in the labour force will reach retirement age
Destatis: by 2039, around 13.4 million people in the labour force will reach retirement age

Where DDD doesn't pay off

Now for the uncomfortable part. If you take DDD seriously, you apply it selectively. Evans recommends concentrating the greatest modelling effort and the best people on the core domain.

For generic subdomains, building your own is usually the wrong decision. Mature standard software exists for financial accounting, payroll and time tracking, and no customer buys from you because your payroll is particularly elegantly modelled. We've worked through the trade-off behind this in detail in our article SaaS vs. customised software.

Pure data maintenance applications don't need DDD either. An internal tool that displays and stores master data has hardly any business rules. Aggregates and domain events would be pure overhead there; a simple CRUD backend is entirely sufficient.

Is Domain-Driven Design worth it? Criteria for the decision
Criterion In favour of DDD Against DDD
Business rules Many rules, exceptions, state transitions Mainly displaying and storing data
Competitive relevance Core domain that sets you apart from the competition Generic process that everyone handles the same way
Lifespan System is meant to last ten years or more Short-lived tool or prototype
Domain knowledge Held by a few employees Fully documented or trivial
Departments involved Several departments, each with its own view One team, one view

If three or more rows land on the left, at least strategic design pays off. You only need tactical patterns for the parts that are genuinely complex.

DDD in practice: how to get started in four steps

It all starts with a room full of domain experts and a big wall. Classes and folder structures come later. The four steps follow the machine builder from the introduction.

Introducing DDD in four steps: Event Storming, core domain, bounded contexts, modular monolith
Introducing DDD in four steps: Event Storming, core domain, bounded contexts, modular monolith

Step 1: Event Storming with business and development

Event Storming is a workshop format that Alberto Brandolini introduced in 2013. Domain experts and developers stick domain events on orange sticky notes together, phrased in the past tense: "Offer accepted", "Configuration checked", "Production order released", "Machine delivered". The events are sorted along a timeline. Then the commands that trigger them go on blue notes. The team marks open questions and conflicts as hotspots.

The real value comes from the discussions. When the head of sales and the head of production planning argue for ten minutes about whether "order confirmed" happens before or after the technical feasibility check, you've found a context boundary. Usually both are right, each in their own context, and that's exactly where the boundary runs.

A workshop for one core process typically takes one to two days. It makes the later requirements documentation much more precise. How to turn the results into a robust document is covered in our guide to writing requirements and functional specifications.

A workshop without domain experts is worthless. A typical failure pattern: the team reads Evans, renames folders to domain, application and infrastructure, and still doesn't talk to the business department afterwards. The result is new developer jargon that nobody outside the team understands.

Event Storming: domain events on orange sticky notes along a timeline, with a hotspot
Event Storming: domain events on orange sticky notes along a timeline, with a hotspot

Step 2: Name the core domain

The workshop produces a map of the subdomains. Now comes the question that management and IT have to answer together: where do we make our money? In the workshop, someone usually says "everywhere" first. After half an hour of discussion, a single area almost always remains. For the machine builder, it's variant configuration. For a wholesaler, it might be pricing for framework agreements; for a logistics company, dispatching.

This area deserves the best team and the most thorough modelling. For everything else, the rule is: buy, integrate or build as lean as possible. This prioritisation is also the foundation of every make-or-buy decision.

Step 3: Cut bounded contexts and draw the context map

Now the boundaries get set. Each bounded context gets a glossary that records its ubiquitous language: if something is called a "production order" there, it's called that in the code too, not ProdOrder or po_tmp.

The glossary only stays alive as long as the team uses it, though. Look away for six months and you'll suddenly find Auftrag and Order side by side in the same module, and nobody knows any more whether they are two things or one. New terms therefore belong in tickets, code reviews and meetings, and get changed as soon as the business department speaks differently.

Next, you draw how the contexts connect. DDD calls this a context map. There's one pattern you'll almost always need: the anticorruption layer. It sits between your context and the ERP and translates the ERP's data model into your language. Imagine an ERP release renames a field. Without a translation layer, you hunt for the spot across the entire system. With one, it's one class, one test, one deployment, and the business logic stays untouched.

Context map with bounded contexts and an anticorruption layer in front of the ERP
Context map with bounded contexts and an anticorruption layer in front of the ERP

Step 4: Modular monolith instead of microservices

This is where many teams stumble a second time. They equate a bounded context with a microservice and start with twelve services, twelve deployments and a message broker. For a team of five, that's operational self-harm. Back in 2015, Martin Fowler advised in his article "MonolithFirst" against starting new projects with microservices: stable boundaries are hard to draw correctly at the start, and refactoring across service boundaries is far more expensive than within one codebase.

The better target architecture for most SMEs is a modular monolith: one application, one deployment, but internally strictly separated modules, one per bounded context, that only talk to each other through defined interfaces.

In a NestJS backend, you map each context as its own module with its own database schema. Within a module, a hexagonal architecture (ports and adapters) separates the business logic from the database and the interfaces. If a module later really becomes a bottleneck, you extract it as a separate service. The boundary already exists by then. There's more on the backend side in our article on enterprise backend architecture.

Shopify is the best-known example of this approach. In 2019, the company described how it was restructuring its huge Rails codebase into a modular monolith. More instructive is the 2024 retrospective on Packwerk, the checking tool it built for this purpose: the package boundaries drawn along business domains often didn't match how the code actually fits together at runtime.

Since then, Shopify has defined packages by how the pieces of code actually depend on each other, and uses the business domains as an overarching umbrella so that teams can own them.

Tools can check boundaries, but they can't find them.

That's not an argument against DDD. It's an argument against believing the first cut is already the right one. The cut from the workshop is a hypothesis that has to prove itself against real code. In a monolith, a wrong cut costs a refactoring sprint. Between two services, it costs a migration including moving data.

For modernising a legacy application, this means extracting one bounded context at a time from the old system, following the strangler fig pattern. We've described how this works in our article App update or rebuild; it works the same way for backends.

Tactical DDD: only where it counts

Within the core domain, the tactical patterns pay off because they put business rules into the domain objects themselves. In legacy software, you often find the same pricing rule in three controllers and a database trigger. Two patterns deliver the most value.

A value object is a value that brings its own rules with it: a delivery date, for example, has to lie in the future. An aggregate is a cluster of objects that may only be changed via one root, the aggregate root.

In plain terms: the order looks after its own line items. Nobody pushes a line item into a confirmed order from outside, because the only way in is through the order, and the order says no. In TypeScript, it looks like this:

type LineItem = { sku: string; quantity: number };

// Value object: a delivery date checks its own rules
export class DeliveryDate {
  private constructor(private readonly value: Date) {}

  static on(date: Date, today = new Date()): DeliveryDate {
    if (date <= today) {
      throw new Error('Delivery date must be in the future');
    }
    return new DeliveryDate(new Date(date));
  }

  get date(): Date {
    return new Date(this.value);
  }
}

// Aggregate root: only the order itself changes its line items
export class Order {
  private lineItems: LineItem[] = [];
  private status: 'open' | 'confirmed' = 'open';
  private scheduledDate?: DeliveryDate;

  addLineItem(lineItem: LineItem): void {
    this.ensureOpen();
    this.lineItems.push({ ...lineItem });
  }

  confirm(deliveryDate: DeliveryDate): void {
    this.ensureOpen();
    if (this.lineItems.length === 0) {
      throw new Error('An empty order cannot be confirmed');
    }
    this.status = 'confirmed';
    this.scheduledDate = deliveryDate;
  }

  get deliveryDate(): DeliveryDate | undefined {
    return this.scheduledDate;
  }

  private ensureOpen(): void {
    if (this.status === 'confirmed') {
      throw new Error('Confirmed orders are locked');
    }
  }
}

The code reads almost like the sentences spoken in the workshop. When the dispatcher says a confirmed order must not be changed any more, a new developer finds this rule in exactly one place.

What you don't need is ceremony. Repositories are a DDD pattern, but Evans intends them per aggregate, not per table. CQRS and Event Sourcing are separate architectural options for specific problems, such as very different read and write loads or a complete change history. Use them when you really have that problem.

One side effect is gaining weight right now: AI coding agents. When an agent works in a cleanly cut module with unambiguous terms, it doesn't have to guess what status2 might mean. You can hand it the glossary for each bounded context directly as working material.

FAQ on Domain-Driven Design

What is Domain-Driven Design, in simple terms?

DDD is an approach to building software along a company's business domain. Developers and the business department develop a shared language, divide the system into clearly delimited areas and invest the most effort where the business differs from the competition.

Is DDD the same as microservices?

No. Bounded contexts are good candidates for service boundaries, but DDD works just as well in a single application.

How long does an Event Storming workshop take?

One to two days is typical for a single core process. How quickly the first cleanly cut module goes live afterwards depends on the state of the legacy system and the availability of the domain experts.

Is DDD worth it for small teams?

Strategic design pays off as soon as several departments with their own views are involved, even with two developers.

Conclusion: language first, then architecture

The order table from the introduction doesn't disappear with a new framework. It disappears when sales, production and service each get their own model and the team speaks their language. Only then can you see which of these models makes your money and which one you can simply buy.

My recommendation: start with an Event Storming workshop for the process that brings your company the most money or causes the most trouble. Name the core domain, cut two or three bounded contexts and build them as modules in one application. Tactical patterns come in when the rules demand them.

If you want to re-cut a legacy custom software system or set up a new system cleanly from the start, talk to us about your enterprise software.

Ready for the next step?

Put what you've learned into practice — we'll support you.

Related posts