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:
| Perspective | What the boundary is made of |
|---|---|
| Technical | Dependencies among classes and interfaces |
| Physical | A common database, common facilities |
| Organizational | The 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.
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.
The question posed by the lecture is: how to preserve the integrity of the model? The reasoning proceeds in four moves:
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.
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.
Best when: multiple contexts share the same team, organization or product.
Key idea: factorise common portions of the model into a shared kernel.
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.
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.
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).
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.
| Layer | Contains | Depends on |
|---|---|---|
| Domain | The domain model: entities, values, events, aggregates. Must support a wide range of applications | No other layer |
| Application | Services providing business logic; supports a particular use case | The domain layer |
| Presentation | Conversion facilities to and from representation formats: JSON, BSON, XML, YAML, XDR, Avro, HTML | The domain layer (and possibly the application layer) |
| Storage | Persistent storage and retrieval of domain data; this is where repositories are implemented; may involve some DB technology | The 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 HTTP | The 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.
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:
:presentation; the DB client library is a dependency of :storage. Neither can reach :domain, because the build system will not let it.:web-api, :message-queue and :command-line all sit on top of :presentation. Adding a fourth way in does not touch the model.product) depends on the outermost modules only, which is what makes the assembly swappable.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.
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.
| Benefits | Limitations |
|---|---|
|
|
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.
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.
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.
| Strategy | When commands are reified | Consequence |
|---|---|---|
| Eager | As soon as they are received | Reads are always cheap; the write path pays |
| Pull | Upon reading queries | Writes stay cheap; the first read after a burst pays |
| Push | In background, periodically | Both paths stay cheap; the staleness window is the price |
The three strategies are explicitly non-mutually-exclusive.
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.
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.
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.
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.
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.
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.
Technical (dependencies among classes and interfaces), physical (a common database, common facilities) and organizational (the people and teams maintaining or using the code).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.