Part II — Domain-Driven Design · Chapter 4

Bounded contexts, model integrity and architecture

~35 min read4 interactive widgets4 plates

In this chapter

  1. Bounded contexts and context maps
  2. The model integrity problem
  3. Upstream and downstream: the anatomy of a relation
  4. The four model integrity patterns
  5. Layered and hexagonal architecture
  6. Enforcing the architecture in the code
  7. Event sourcing
  8. CQRS
  9. The exercises
  10. Check your understanding

1. Bounded contexts and context maps

Two further notions involving contexts complete the vocabulary of Chapter 2:

The definition of a context boundary is worth reading slowly, because it names three independent perspectives from which a boundary is real:

PerspectiveWhat the boundary is made of
TechnicalDependencies among classes and interfaces
PhysicalA common database, common facilities
OrganizationalThe people and teams maintaining or using the code

A context map is then a map of all the contexts in a domain and their boundaries, plus their points of contact — dependencies, homonyms, false friends — providing the whole picture of the domain.

Best practices

Key idea

The last best practice is the bridge between this course's two halves. A boundary that is only drawn on a whiteboard erodes at the first deadline; a boundary that is checked by a test suite running on every commit is an executable claim. This is the same reasoning that will make CI/CD non-negotiable at the exam, and the same reasoning that makes MLOps insist on automated evaluation in Chapter 6.

2. The model integrity problem

The question posed by the lecture is: how to preserve the integrity of the model? The reasoning proceeds in four moves:

  1. As the domain evolves, the software model should evolve with it, in order to maintain the coupling. (Recall Chapter 2: adherence must hold at any moment.)
  2. Yet the domain rarely changes as a whole: more commonly it changes in a context-specific way.
  3. Contexts are bounded, but not isolated — and so are models, which may depend on each other.
  4. Therefore changes to a context and its model may propagate to other contexts and models.

Conclusion, stated flatly on the slide: domain and model changes are critical and should be done carefully.

The four model integrity patterns are the answers, and they share two purposes: preserve the integrity of the model with respect to the domain, and minimise the potential impact and reach of changes — each context as independent as possible, each change affecting as few contexts as possible.

3. Upstream and downstream: the anatomy of a relation

Context maps highlight relations among contexts, yet not all relations are equal, nor symmetric. Each relation between two contexts usually involves two ends, or roles:

Then comes the sentence that turns an architectural discussion into an organisational one:

Integration among contexts ↔ interaction among teams.

Several strategies may be employed, depending on: mutual trust among teams, ease of communication and cooperation among teams, and technical, organizational, administrative or legal constraints. The lecture even defines the term it is about to use as a variable: trust ≈ willingness to collaborate + seek for stability.

4. The four model integrity patterns

Best when: multiple contexts share the same team, organization or product.

Key idea: factorise common portions of the model into a shared kernel.

  • Upstream and downstream collaborate in designing, developing and maintaining the model — they are peers.
  • Keeping the kernel as small as possible is fundamental.

The size warning is not cosmetic: everything inside the kernel is a change that must be negotiated by two parties, so a fat kernel converts an integration problem into a coordination bottleneck.

Best when: multiple teams, mutual trust, good communication.

Key idea: the upstream acts as supplier, the downstream acts as customer; both sides collaborate to maximise integration among their models and interoperability among their software.

  • Customers may ask for features, and suppliers will do their best to provide them.
  • Suppliers shall warn before changing their model.

The distinguishing feature with respect to the shared kernel: the two models stay separate. What is shared is a commitment, not a code base.

Best when: multiple teams, poor communication or different pace, some trust.

Key idea: the downstream must conform to the upstream, reactively — adapting their model accordingly, whenever the upstream's one changes.

Note the asymmetry with customer-supplier: here the downstream has no say. It gives up the ability to negotiate in exchange for not having to translate, which is affordable only if the upstream model is acceptable in the first place (hence "some trust").

Best when: multiple teams, poor communication, poor trust.

If the upstream cannot be trusted and interaction is pointless — legacy code, a poorly maintained library — then the downstream must defend itself from unexpected or unanticipated change. The upstream's model is reverse engineered and adapted behind a layer.

The lecture gives the everyday example: often, repository types are anti-corruption layers for DB technologies. That is the same repository block from Chapter 3, seen from the integrity angle.

For the exam

Learn the four patterns with their selection conditions, not as four names. The discriminating questions are always the same three: one team or several? is there mutual trust? is communication good? Then: shared kernel (one team/org/product, peers), customer-supplier (several teams, trust, good communication), conformist (poor communication, some trust, downstream adapts reactively), anti-corruption layer (poor communication, poor trust, downstream translates and defends).

5. Layered and hexagonal architecture

The lecture opens this part with a disclaimer that should be quoted before any architecture debate:

The organising rule of the whole arrangement is a single sentence: outer layers depend on innermost ones; the vice versa is not true.

LayerContainsDepends on
DomainThe domain model: entities, values, events, aggregates. Must support a wide range of applicationsNo other layer
ApplicationServices providing business logic; supports a particular use caseThe domain layer
PresentationConversion facilities to and from representation formats: JSON, BSON, XML, YAML, XDR, Avro, HTMLThe domain layer (and possibly the application layer)
StoragePersistent storage and retrieval of domain data; this is where repositories are implemented; may involve some DB technologyThe domain layer (and possibly the presentation layer)
Interface (ReST API, MOM, View)Lets external entities access the software, via a GUI or a remote interface such as HTTPThe layers beneath

Notice how the table restates a distinction from Chapter 3 in architectural terms: the domain layer must support a wide range of applications (entities are general purpose) while the application layer supports a particular use case (services are purpose-specific). The design asymmetry between services and entities is the boundary between two layers.

6. Enforcing the architecture in the code

A layering that exists only in a diagram is a layering that will be violated by the first hurried import. The lecture therefore states that layering may be enforced in the code, by mapping layers into modules, where module ≈ packaging unit — Gradle sub-projects, Maven modules, .NET assemblies — each module having its own build dependencies.

The component graph given on the slide reads as follows, with <|-- meaning "is depended upon by":

:domain        <|--  :application
:application   <|--  :presentation
:presentation  <|--  :web-api
:presentation  <|--  :message-queue
:presentation  <|--  :command-line
:application   <|--  :storage
:presentation  --|>   third-party serialization library (e.g. gson)
:storage       --|>   third-party DB client library
product        --|>   :web-api , :storage , :message-queue

Three observations make this worth memorising rather than merely reading:

Editor's note

This is the point where DDD stops being a modelling philosophy and becomes a build configuration. A dependency rule expressed in build.gradle is checked by every compilation, by every developer, on every machine, forever — the same "make the claim executable" move as the automated tests that enforce a context's cohesion.

7. Event sourcing

The preliminaries are put as an observation about mutable entities: whenever there is a mutable entity whose state evolution over time must be tracked, state transitions can be memorised in two ways — one may track the current state, or the flow of variations.

Event sourcing is then defined as a pattern where domain events are reified into time-stamped data and the whole evolution of a system is persistently stored. The lecture calls it a perfect match with DDD, as domain events are first-class citizens — the block from Chapter 3 becomes the unit of persistence.

BenefitsLimitations
  • Historical data can be analysed, for predictive maintenance, optimization, analysing and anticipating faults
  • Past situations can be replayed, which improves debugging and enables measurements
  • Enables complex event detection and reaction
  • Enables CQRS
  • A lot of data is generated and must be stored, which costs space
  • Reconstructing the (current) state costs time

8. CQRS

Command-Query Responsibility Segregation is introduced as an advanced pattern for building highly-scalable applications. It leverages upon event sourcing and layered architecture to deliver reactive, eventual-consistent solutions where context boundaries can be easily enforced and the single responsibility principle is applied extensively.

The definition is one line: split the domain and application layers to segregate read/write responsibilities.

The two workflows

Writing. Whenever users are willing to perform an action into the system: they create a command and forward it to the write model — an object describing a variation to be applied to some domain aspect; the command is possibly validated and stored onto some database, an ad-hoc database being available in the model for storing commands.

Reading. Whenever users are willing to inspect or observe the system at time t: they perform a query on the read model, asking for the state of the system at time t (for instance t = now); commands up to time t are assumed to be reified when reading, and a snapshot of the system state at t is returned.

When are commands reified?

Reification is defined as the process of computing the state of the system at time t by applying the commands recorded up to time t. If queries and commands are stored on different databases, reification implies updating the query database — and then a nice piece of engineering advice follows: the query database should be read-efficient, the commands database should be write-efficient.

StrategyWhen commands are reifiedConsequence
EagerAs soon as they are receivedReads are always cheap; the write path pays
PullUpon reading queriesWrites stay cheap; the first read after a burst pays
PushIn background, periodicallyBoth paths stay cheap; the staleness window is the price

The three strategies are explicitly non-mutually-exclusive.

9. The exercises

Three exercises accompany the DDD module, on the repository github.com/unibo-spe/ddd-exercise, branch exercise (solutions on branch master). They are the practical closure of Chapters 2 to 4, and they map one-to-one onto the three themes of this chapter.

Exercise 1 — Simple Store

A simple domain keeping track of customers, products and orders:

The to-do list is the workflow of Chapters 2 to 4 in miniature: read the informal domain description; identify the main concepts composing the ubiquitous language; model the domain as Java types; the model should include entities, value objects, repositories, factories and services; structure the Java types according to a module structure compliant with hexagonal architecture (put code into either the domain or the application module); sketch tests and then implementations for at least one entity, value object, factory and repository.

Key idea

Two sentences in that brief are traps that reward a careful reader. "Both the name and email may vary over time" forces Customer to be an entity with an identifier. "When a new order is registered, many actions should be performed in reaction" is the textual signature of a domain event.

Exercise 2 — Trivial CQRS

A very simple repository type, the Counter: it contains one long number, initially 0; the value may be read or changed arbitrarily; whenever the value changes a new domain event of type Variation is published; however, the repository only memorises the current value of the counter.

To do: switch the design towards event sourcing, by memorising variations instead of snapshots; then implement CQRS by splitting the repository into two parts — a write-model for storing variations and a read-model for retrieving a snapshot of the counter's value in a given moment. In practice: provide implementations for the CounterReader and CounterWriter interfaces, and optionally test them.

Exercise 3 — Anti-corruption layer

A very simple domain, Tables: two-dimensional containers of Rows, where each row contains one or more String values. Functionalities for CSV import/export are missing and need to be implemented via third-party libraries — Apache Commons CSV or OpenCSV.

To do: extend the model with new interfaces supporting CSV parsing and writing; design the interfaces so that they are agnostic of the third-party libraries, without corrupting the domain model with library-specific types; implement them using one library; sketch tests; then implement them again using the other library; and use the same tests to prove the two implementations work the same way.

For the exam

Exercise 3 is the anti-corruption layer reduced to its essence, and it also states the criterion by which such a layer is judged: if the abstraction really isolates the domain, then the same test suite must pass against two different vendor implementations. That is a falsifiable claim about a design — exactly what the course wants you to be able to make.

Check your understanding

From which three perspectives should a context boundary be explicit?

Technical (dependencies among classes and interfaces), physical (a common database, common facilities) and organizational (the people and teams maintaining or using the code).

State the four best practices for bounded contexts and context maps.

Clearly identify and represent boundaries; avoid responsibility diffusion over a single context (one responsible person or team per context); avoid changing the model because of problems arising outside the context — extend the domain with new contexts instead; and enforce a context's cohesion via automated unit and integration testing, re-executed as frequently as possible.

Why is model integrity a problem at all?

Because the model must keep matching the domain while the domain evolves, but the domain rarely changes as a whole — it changes context by context. Contexts are bounded but not isolated, so their models depend on each other, and a change in one may propagate to others. Hence: changes are critical and must be done carefully.

Define the upstream and downstream ends of a relation between contexts.

The upstream provides functionalities; the downstream consumes them; the downstream depends upon the upstream, but not vice versa. This asymmetry is what makes the four integrity patterns necessary, and the lecture pairs it with the slogan integration among contexts ↔ interaction among teams.

Which pattern fits multiple teams with mutual trust and good communication, and what does each side commit to?

Customer-supplier. The upstream acts as supplier and the downstream as customer; both collaborate to maximise integration and interoperability. Customers may ask for features, suppliers do their best to provide them, and suppliers shall warn before changing their model.

Distinguish conformist from anti-corruption layer.

Both apply when communication is poor. Conformist assumes some trust: the downstream adapts its own model reactively whenever the upstream changes. Anti-corruption layer assumes poor trust: interaction is pointless (legacy code, a poorly maintained library), so the upstream model is reverse engineered and adapted behind a defensive layer. Conformist accepts the foreign model; the ACL translates it.

Why must a shared kernel be kept as small as possible?

Because it is jointly owned: upstream and downstream collaborate as peers on everything inside it. Every element of the kernel is therefore a change that needs bilateral agreement, so a large kernel converts an integration mechanism into a coordination bottleneck.

Does DDD require the hexagonal architecture?

No. DDD does not enforce a particular architecture, and any is fine as long as the model is integer. Layered architectures are simply well suited to preserving model integrity, and the hexagonal architecture is the particular case studied because it fits DDD well.

State the dependency rule of the hexagonal architecture and the role of each layer.

Outer layers depend on innermost ones; never the reverse. Domain: the model, supporting a wide range of applications, with no dependency on any other layer. Application: services implementing the business logic for a particular use case, depending on the domain. Presentation: conversion to and from representation formats (JSON, XML, YAML, Avro, HTML...). Storage: persistence, where repositories are implemented. Interface layers (ReST API, MOM, View): access from the outside.

How is the layering enforced in the code?

By mapping layers into modules (packaging units: Gradle sub-projects, Maven modules, .NET assemblies), each module having its own build dependencies. The third-party serialization library then belongs to :presentation and the DB client library to :storage, so neither can reach :domain — the build system refuses.

What is event sourcing, and what does it cost?

A pattern where domain events are reified into time-stamped data and the whole evolution of the system is persistently stored — the flow of variations instead of the current state. It buys analysis of historical data, replay of past situations, complex event detection, and CQRS. It costs storage space (a lot of data is generated) and time to reconstruct the current state.

What does CQRS segregate, what is reification, and when does it happen?

CQRS splits the domain and application layers to segregate read and write responsibilities: a write model accepting commands (objects describing a variation to apply) and a read model accepting queries (returning a snapshot of the state at time t). Reification is the process of computing the state at t by applying the commands recorded up to t. Three non-mutually-exclusive strategies decide when it happens: eager (as soon as commands are received), pull (upon reading queries) and push (in background, periodically). When queries and commands live in different databases, the query database should be read-efficient and the commands database write-efficient.