← Back to the section

Over time, most serious projects turn into systems that are hard to change. DDD is an approach that helps you avoid this by tying the structure of your code to the business domain. Let's start from scratch: what the problem is, how DDD solves it, and when you actually need it.

The problem: business and code speak different languages

Picture a typical enterprise project a couple of years into development:

  • The business says "application", the code is a total mess. Request, Order, Ticket, and Application in different services — all about the same thing.
  • Changing one business rule breaks everything. "Add a discount for VIP customers" turns into editing 15 files, because the logic is smeared across handlers, services, and SQL queries.
  • Modules drag each other along. The payment module knows about the warehouse, the warehouse knows about notifications. Any change triggers a cascade.
  • A new person drowns. Onboarding takes months, because there is no explicit structure and no shared language.

Domain-Driven Design (DDD) is an approach in which the structure and language of the code are tied to the business domain: to the way the business describes its own processes. Not to the framework, not to the database, not to the API. The term was coined by Eric Evans in his book "Domain-Driven Design: Tackling Complexity in the Heart of Software" (2003).

Ubiquitous language — the team's shared language

The first thing DDD starts with: developers and business experts agree on a shared vocabulary. That vocabulary is called the ubiquitous language.

What this means in practice:

  • the terms in the code match the terms used in conversations with the business;
  • if the business says "application", the class is named Application, not Request or Ticket;
  • when a new business term appears — it immediately appears in the code.

Without a shared language, a "translation" arises between what the business wants and what is written in the code. Translation means losses: inaccuracies, bugs, and time wasted on clarification.

Bounded context — the boundary where words are unambiguous

The same word can mean different things in different parts of the system. A "customer" in the sales department is a prospective buyer. A "customer" in accounting is a payer with billing details. An "order" at the moment it is placed and an "order" in the warehouse are completely different sets of data.

A bounded context is an explicit boundary within which terms have an unambiguous meaning. Each context lives by its own model, and that's fine: there is no need to torture a single universal "Order" model trying to please everyone.

One context — one team, one set of terms, one language. At the intersections of contexts there are explicit rules of interaction.

Aggregate — the boundary of change

Within a single context, data does not exist in isolation: objects are linked to one another. But if you change them any which way, integrity breaks.

An Aggregate is a cluster of related objects with a single root object (Aggregate Root). All changes go only through the root. This ensures integrity: you cannot change an order line without going through the order itself.

Example: Order is the aggregate root, OrderLine is an internal object. You cannot add a line while bypassing the Order.addLine(...) method. The aggregate enforces its own rules.

The aggregate boundary is also the transaction boundary: one operation changes one aggregate.

Domain Event — what happened in the domain

When something important happens in the system, other parts need to be told about it. A Domain Event is a fact that has already occurred in the domain.

Examples: OrderConfirmed ("the order was confirmed"), PaymentReceived ("the payment was received"), ItemShipped ("the item was shipped").

The key word is "happened". An event describes the past, not a command for the future. Other contexts can react to an event, but they do not dictate how it is handled inside the source.

Events make contexts loosely coupled: the payment service knows nothing about delivery, it simply publishes PaymentReceived, and delivery subscribes and reacts.

Strategic and tactical patterns

DDD works on two levels:

The strategic level — how to split the system into parts:

  • identify the bounded contexts and their boundaries;
  • draw a Context Map — who talks to whom;
  • single out the Core Domain (the heart of the business, where complexity is highest) and the supporting subdomains.

The tactical level — how to write code within a single context:

  • Aggregate and Aggregate Root;
  • Domain Event;
  • Value Object (a value object without identity: money, an address, coordinates);
  • Repository (an abstraction over the aggregate's storage);
  • Domain Service (logic that does not belong to any single aggregate).

A common mistake is to jump straight into the tactical patterns without figuring out the strategic ones first. First you need to understand the boundaries and the language, and only then write code.

When DDD pays off and when it doesn't

DDD is a tool for complexity. Where there is no complexity, it will add some of its own.

DDD pays off when:

  • the business logic is non-trivial and changes often;
  • the project is long-lived — years, not months;
  • several teams work on the same system;
  • the domain is complex: a marketplace, finance, logistics, insurance;
  • developers and business experts can talk regularly.

DDD is overkill when:

  • the application is a simple CRUD with no substantial business logic;
  • a small team, a single context, clear requirements;
  • a prototype that needs to launch quickly;
  • there is no access to business experts.

A good rule of thumb: if all your business logic fits into a single if in a request handler — you don't need DDD. If the business rules take hundreds of lines and keep growing — it's worth considering.

How to get started

If you've decided to give it a try:

  1. Talk to the business. Event Storming is a good format to start with: you gather the team and write out all the events in the domain.
  2. Record a glossary. The list of terms is your ubiquitous language. Let it live in a document and keep growing.
  3. Identify the contexts. Draw a Context Map — who talks to whom, who depends on whom.
  4. Start with one context. Pick the Core Domain and apply the tactical patterns. Don't try to rewrite everything at once.

In short

  • DDD ties the structure of the code to the business domain, not to a framework or a database.
  • Ubiquitous language — the shared vocabulary of developers and the business; the terms in the code match the business terms.
  • Bounded context — an explicit boundary within which terms are unambiguous; each context has its own model.
  • Aggregate — a cluster of objects with a root; all changes go through the root, one transaction — one aggregate.
  • Domain Event — a fact that has occurred; it makes contexts loosely coupled.
  • The strategic level (boundaries, Context Map) comes first; the tactical patterns come second.
  • DDD pays off with complex and changing business logic; for a simple CRUD it is overkill.
  • DDD Strategic Patterns — bounded context, Context Map, subdomain types.
  • DDD Tactical Patterns — Aggregate, Entity, Value Object, Repository, Domain Service.
  • DDD Integration Patterns — how contexts talk to each other.