We went over the Tactical side of Domain Driven Design here. Now, we take on its various strategies.
When linking together the two parts for a more general 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
Use the same language everywhere possible, and let that language be dictated by the Domain at hand. 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.
Let the code reflect the business language.
One of the great advantages of using Ubiquitous Language is to bring together Domain experts, technical team, and others involved in the project, with very little to no ambiguity.
In order for you to develop an efficient Ubiquitous Language you need to understand the business and the Domain, which you should strive to do anyway.
Bounded Context
A Bounded Context is a linguistic and/or conceptual delimitation.
Same concepts might have different implications in different contexts. These contexts more or less reflect the business structure of the enterprise at hand.
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 the bounded context depends on the needs and possibilities we have.
You donβt need tight, perfect, future-proof, bounded contexts. Rather, you need enough flexibility 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 have a structure like:
Apps
These are 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 a Use Case execution.
You will commonly find various Applications per Bounded Context and their directory structure usually reflects this relationship.
So adding these concepts to our previous example:
Context Maps
Visual representation of a systemβs Bounded Contexts and relationships between them.
This helps in understanding the project as a whole (high-level design) as well as showing the communication patterns between contexts.
Keep in mind that one of the main benefits of DDD is that it allows multiple teams to simultaneously work on different parts of the same project. These βpartsβ usually come down to our Bounded Context. As such, building a context map will also show organizational issues, bottlenecks and team dependencies.
These are some ways Bounded Contexts (and/or teams) might relate to one another:
Client - Server (Customer - Supplier)
Pretty self-explanatory: One Bounded Context is upstream while another one is (or multiple ones are) downstream. This makes them somewhat independent, but ultimately one of the can dictate the integration contract.
Anti-corruption layer
Another upstream/downstream relationship, where the downstream Bounded Context implements a layer responsible for translating upstream objects into its own.
Mostly used to separate the old, legacy part of the system from the greenfield. It allows you to treat that part of the codebase as a βblack boxβ of sorts.
Shared Kernel
A more thoughtful version of the Utils directory.
Here, a common contract is defined and referenced by both (or 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 more than two Contexts (or Modules), and only when they are needed. So a classic example is 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.