MODULE 13 OF 6 · APPLIED

Domain-Driven Design Essentials

30 min read 4 outcomes Interactive quiz

By the end of this module you will be able to:

  • Explain the ubiquitous language and how it connects code to business concepts
  • Define bounded context and explain why the same term can mean different things in different parts of a system
  • Describe aggregates and explain why invariants must be enforced within the aggregate boundary
  • Apply the context map to show how bounded contexts relate and which integration pattern fits each relationship
Team meeting around a whiteboard (photo from Unsplash)

Real-world case · 2003 to present

Three teams, one word, three different meanings. The bug that launched a book.

Eric Evans spent years consulting on large software projects before writing Domain-Driven Design in 2003. The pattern he observed most consistently was this: teams that worked in the same codebase but on different parts of the business used the same words to mean different things. Sales used "customer" to mean a prospect being cultivated. Finance used "customer" to mean an entity with payment terms. Support used "customer" to mean a user with open tickets.

When a single Customer class in a shared codebase tried to serve all three teams, it accumulated fields for all three meanings. AddingnextAppointment for the scheduling team broke the billing team's validation logic. Adding insuranceProvider for the billing team confused the support team's ticket display. The class grew without bounds and changes for one team regularly broke the others.

Evans' diagnosis was that the problem was not technical but linguistic: the codebase had no explicit boundaries around where each meaning applied. His solution was the bounded context: an explicit boundary within which a term has a single, consistent meaning. Within the Billing context, Customer means one thing. Within the Scheduling context, it means something different. Both meanings coexist in the system because they are in different bounded contexts with their own models.

Your team says 'customer.' The billing team says 'customer.' The support team says 'customer.' If all three mean different things, what happens when you share a single Customer class?

With the learning outcomes established, this module begins by examining ubiquitous language in depth.

13.1 Ubiquitous language

Domain-Driven Design (DDD), introduced by Eric Evans in 2003, begins with language. The ubiquitous language is a shared vocabulary developed jointly by developers and domain experts and used consistently in code, tests, documentation, and conversation. The language is "ubiquitous" within a bounded context: used everywhere, without translation, by everyone working in that context.

The practical effect on code is concrete. A method calledvoid processPayment() could mean charge the card, mark an invoice paid, or transfer funds between accounts. A method calledvoid settleTrade(Trade trade, SettlementDate date) communicates exact business intent that a domain expert can read and confirm. Class names, method names, and variable names should use the language that domain experts use, not technical abstractions that developers invented.

Violations of the ubiquitous language are indicators of model problems. When developers translate business concepts into different technical names, knowledge is lost at the translation point. A domain expert reviewing the code cannot recognise their own concepts. Bugs hide in the translation gap. The closer the code language is to the business language, the more confidently domain experts can validate the model.

Use the model as the backbone of a language. Commit the team to exercising that language relentlessly in all communication within the team and in the code. Use the same language in diagrams, writing, and especially speech.

Eric Evans - Part I, Putting the Domain Model to Work

Evans is saying that the language is not documentation layered on top of the code; it is the code. If the team talks about 'settlements' but the code says 'finalisedTransactions', there is a translation step that introduces errors. The language must be ubiquitous: the same word means the same thing everywhere it appears.

With an understanding of ubiquitous language in place, the discussion can now turn to bounded contexts, which builds directly on these foundations.

13.2 Bounded contexts

A bounded context is an explicit boundary within which a particular domain model is defined and consistent. The same term can mean different things in different bounded contexts; the context makes the meaning unambiguous.

In an e-commerce platform, an Order in the Ordering context is a customer's requested purchase with line items and a shipping address. The same word in the Fulfilment context refers to a picking instruction for a warehouse worker, with bin locations and carrier details. In the Billing context, an order is the trigger for generating an invoice with payment terms. All three models are correct within their context; none of them is the universal truth.

Bounded contexts should align with team boundaries. Conway's Law predicts that the architecture of a system mirrors the communication structure of the organisation that built it. If a single team owns the entire codebase, the bounded contexts will be informal and poorly enforced. If the Billing team owns the Billing context, they own the Customer model within that context and no other team can change it without their agreement.

Common misconception

Each microservice should have its own bounded context.

Bounded contexts and microservices are related but not equivalent. A bounded context is a conceptual boundary defined by language and domain model; a microservice is a deployment unit. One bounded context may be implemented as multiple microservices. Multiple small contexts may share a service for operational simplicity. Identify the bounded contexts first from the domain, then decide on the deployment structure. Starting with microservices and trying to fit bounded contexts to them produces incorrect boundaries.

With an understanding of bounded contexts in place, the discussion can now turn to aggregates and invariants, which builds directly on these foundations.

13.3 Aggregates and invariants

An aggregate is a cluster of domain objects (entities and value objects) treated as a single unit for the purposes of data changes. Every aggregate has an Aggregate Root: the entity through which all external access passes. External objects cannot hold direct references to entities inside the aggregate; they must go through the root.

Aggregates define consistency boundaries. All changes within an aggregate are atomic: either all changes in a transaction succeed or none do. Changes that span multiple aggregates are eventually consistent, handled through domain events. The aggregate root enforces the invariants: the business rules that must always hold within the aggregate. An order cannot contain a line item with quantity zero. A confirmed order cannot have items added to it. These rules are enforced in the aggregate root, not scattered across application services.

Small aggregates are strongly preferable to large ones. A large aggregate creates a contention hotspot: multiple users attempting simultaneous operations must wait for the same lock. An Order aggregate containing the order, all line items, the customer details, the payment method, and the shipping history will be locked by checkout, payment processing, shipping updates, and customer profile changes simultaneously. Each of those operations should be a separate aggregate with its own lifecycle.

Aggregates are the basic element of transfer of data storage - you request to load or save whole aggregates. Transactions should not cross aggregate boundaries.

Martin Fowler - DDD Aggregate. martinfowler.com

Fowler is defining two constraints that make aggregates useful. Loading and saving whole aggregates (not arbitrary object graphs) makes consistency guarantees explicit. Not crossing aggregate boundaries with transactions forces designers to think about what must be consistent together, and what can be eventually consistent.

Common misconception

Aggregates are just another name for database entities.

Aggregates are designed around invariants and consistency requirements, not around database tables. A single aggregate may span multiple database tables. Multiple small entities may each be separate aggregates even if they are related. The design question is: what must be consistent together in a single transaction? That determines the aggregate boundary. Database normalisation optimises for storage; aggregate design optimises for consistency and behaviour.

With an understanding of aggregates and invariants in place, the discussion can now turn to context maps and integration patterns, which builds directly on these foundations.

13.4 Context maps and integration patterns

A context map is a diagram that shows all the bounded contexts in a system and the relationships between them. It makes integration patterns explicit before the code is written, reducing the probability that a change in one context silently breaks another.

DDD identifies several integration patterns for the relationships between contexts. A Shared Kernel means two contexts share a small, co-owned model; both teams must agree before changing the shared part. ACustomer-Supplier relationship means an upstream context defines an API that a downstream context consumes; changes are negotiated. AnAnti-Corruption Layer (ACL) means the downstream context translates the upstream model into its own; useful when the upstream model is legacy, poorly structured, or owned by an external team with no obligation to accommodate downstream needs.

The most important pattern for microservices is the anti-corruption layer. When the Ordering context publishes an OrderConfirmed event, the Fulfilment context should not use the Ordering context's Orderdata model directly. It should translate the event into its ownFulfilmentRequest model. If the Ordering context changes its event schema, only the ACL needs to be updated; the Fulfilment context's internal model is protected.

13.5 Check your understanding

A hospital system has a single Patient class used by the scheduling, billing, and clinical modules. All three teams constantly break each other's code when modifying the class. Which DDD concept directly addresses this problem?

The scheduling system confirms an appointment. The billing system needs to create an invoice. Which DDD integration pattern is most appropriate?

What is the Aggregate Root, and why must all external changes to an aggregate pass through it?

A team is designing a new e-commerce platform. They propose a single Order aggregate that contains the order, all line items, the customer's full profile, the payment method details, and the complete shipping history. What is wrong with this design?

Having covered the four core DDD building blocks, this section brings them together through an interactive diagram that lets you trace how ubiquitous language, bounded contexts, aggregates, and context maps relate to one another in practice.

Explore the concepts interactively

Use this interactive diagram to explore the concepts discussed in this module. Click on elements to see how they relate to each other and to the patterns covered above.

Loading interactive component...

Key takeaways

  • The ubiquitous language connects code to business concepts: class names, method names, and domain terms should be identical to the words domain experts use, within a bounded context.
  • Bounded contexts give each part of the system its own internally consistent model. The same term can mean different things in different contexts; the boundary makes the meaning unambiguous.
  • Aggregates define consistency boundaries. All changes within an aggregate are atomic; changes across aggregates are eventually consistent through domain events. The Aggregate Root enforces invariants.
  • Small aggregates are preferable to large ones. Aggregate boundaries should be drawn around what must be consistent in a single transaction, not around which objects are related.
  • Strategic DDD (bounded contexts, context maps, integration patterns) delivers most of the architectural value and is applicable even without tactical DDD implementation patterns.

Standards and sources cited in this module

  1. Evans, E. (2003). Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley

    Part I: Putting the Domain Model to Work

    The original DDD text. The quote in Section 13.1 is from Chapter 2. All four core concepts in this module (ubiquitous language, bounded context, aggregate, context map) originate here.

  2. Vernon, V. (2013). Implementing Domain-Driven Design. Addison-Wesley

    Chapters 2, 3, and 10

    The most accessible practical guide to DDD for working engineers. Used for the aggregate sizing guidance in Section 13.3 and the context map integration patterns in Section 13.4.

  3. Fowler, M. DDD Aggregate. martinfowler.com.

    Full article

    The quote in Section 13.3 is from this article. Fowler's concise characterisation of aggregate loading, saving, and transaction boundaries is the clearest statement of the two key aggregate constraints.

  4. Fowler, M. BoundedContext. martinfowler.com.

    Full article

    Concise explanation of bounded context with diagrams. Referenced in Section 13.2 for the relationship between bounded contexts and team ownership.

What comes next: DDD defines boundaries. But distributed systems fail in ways monoliths do not. Module 14 covers resilience and fault tolerance: circuit breakers, retries, bulkheads, timeouts, and the principle that systems must be designed to assume failure rather than prevent it.

Module 13 of 22 in Applied