This is part of a series, start here!
We went over the Tactical concepts of DDD here. In this post, weβll cover the Strategic side.
When linking together the two parts for a more comprehensive picture, pay spacial attention to the concepts of Ubiquitous Language and Bounded Contexts, since these are the bits that keep the whole thing together.
Ubiquitous language
The idea is to use the same language everywhere possible, and let that language be dictated by the Domain and/or the Domain experts.
In an ideal world, we wouldnβt have to map developer-speak to business-speak: we would all be using the same terms to describe the same things (God knows that almost never happens).
Let the code reflect the business language.
One of the advantages of following this approach is bringing together Domain experts, technical team, and other stakeholders involved in the project, with as little ambiguity as possible.
This is often not easy to do: in order to develop this Ubiquitous Language you need to understand the business and the Domain.
Developers also need to accept that they will often not be the ones in charge of naming things. Which frankly, is a very good thing IMO.
Bounded Context
A Bounded Context is a linguistic and/or conceptual delimitation.
The same concepts might have different implications in different contexts.
These contexts more or less reflect the business structure of the enterprise, or the problem domain.
Bounded contexts define isolated parts of the model with some degree of independence.
The isolation can be achieved by decoupling logic, code segregation, database segregation and also in terms of team organization.
The degree to which we isolate Bounded Contexts depends on the needs and realities of the business, and will often be context dependent.
You donβt need tight, completely independent, future-proof, Bounded Contexts.
But you do need enough flexibility in your system to easily promote Modules to Bounded Contexts when needed.
Modules
Bounded Contexts are made up of Modules, which you can think of as mini-Bounded Contexts: Smaller semantic units that make sense within a greater common Context.
Itβs usually a good idea to have only one Aggregate per Module. The need for more than one might indicate a need for a new Module or for the Module to get promoted to Bounded Context.
So to be specific, you could manifest this in a structure like:
Apps
These should be the entry points to our Bounded Contexts.
They are usually called by API controllers, CLI interfaces, etc. and orchestrate use cases.
They lay outside our Bounded Contexts and call (directly or not) the Application Services to initiate use case execution.
There might be various Applications per Bounded Context, and their directory structure usually reflects this relationship.
So adding this to the previous example:
Something like that anyway, these structures are only here to better illustrate the relations between each piece.
Context Maps
Visual representation of a systemβs Bounded Contexts and how they relate to each other.
It helps understand the project as a whole (high-level design) as well as showing the communication patterns between contexts.
One of the main benefits of DDD is that it allows multiple teams to simultaneously work on different parts of the same system.
These βpartsβ usually, though not always, come down to our Bounded Context and as such, building a context map will also show organizational issues, bottlenecks and team dependencies.
These are some ways Bounded Contexts might relate to one another:
Client - Server (Customer - Supplier)
As you might expect, one Bounded Context is upstream while another one is (or multiple ones are) downstream.
This makes them somewhat independent, but ultimately one of them will dictate the integration contract.
Anti-corruption layer
Another upstream/downstream relationship, where the downstream Bounded Context implements a layer responsible for translating upstream objects/structures into its own.
Mostly used to separate the old, legacy part of the system from a greenfield. It allows you to treat that part of the codebase as a βblack boxβ.
Shared Kernel
A more thoughtful version of your typical Utils directory.
Here, a common contract is defined and referenced by multiple bounded contexts.
The key to implementing a shared kernel correctly is to keep its scope as small and limited as possible.
Another less thoughtful but common approach is to have a shared kernel that holds only dumb components that are needed in multiple Contexts (or Modules), and only when they are needed.
So a classic example could be the Value Object for user IDs: Their structure will depend on our Domain, and they will most definitely be used all over the place, while not holding significant logic apart from basic validation. They are also very unlikely to change, so they make for a somewhat safe common dependency.