Segregating the domain into bounded contexts its a good start into maintainability, however if in every bounded context we have a different structure it can and will become cumbersome to understand the current bounded contexts and to further develop new bounded contexts.
For this reason, the team should rely on a simple generic structure to follow in every bounded context. This way, every developer in the team will be familiar with the global structure of every bounded context, making communication and collaboration easier between developers and teams.
Nevertheless, this does not mean the structure must be frozen. It can, and should, evolve as needed.
A system metaphor is a concrete concept, defined in broad strokes.
It is not a domain concept, it is a concept that can be implemented in different, but compatible, ways and can be applied to several projects and domains and/or bounded contexts.
We can identify a System Metaphor when the team starts to consistently use an analogy, which is not part of the Domain but it facilitates communication, increases consistency and directs the project development in a useful direction.
At large scale, segregating responsibilities in layered groups of objects is a common practise (think MVC) for quite some time. It’s like applying the SOLID SRP at large scale, to groups of objects.
However, this groups of objects are layered, where the objects in each layer are aware and may depend on the layer below but completely unaware and idempotent of the layers above.
Layers are partitions of a system in which members of each partition are aware of and are able to use the services of the layers “bellow”, but unaware and independent of the layers “above”.
Despite this, sometimes the lower layers still need to communicate to the top layers. To achieve this, and still have the lower layers decoupled from the top layers, we can use domain events.
Although it is common to think of layers at a functional level (think of View -> Controller -> service -> Repository -> Client), we can, and should, also think of layers at a domain level (think of “concept A module” -> [depends on] -> “Concept B module”). This is extremely useful, and I’m thinking of the situation where we have a code unit (class or set of classes) that relates two modules: We end up asking “In what module should we put this ?!“. The answer is: “Put it in the higher layer module!“
Eric Evans also tells us that when applying the Responsibility Layers Pattern, a Relaxed Layered System, where components can access any lower layer and not just the one immediately below it, is a better choice than a Strict Layered System. This is also a choice that must be made clear to the team.
Choosing the Domain Layers is more of a Business process, and therefore a Conceptual Domain exercise. Eric Evans gives us a few guidelines:
- Storytelling: The layers should reflect the priorities of the Domain;
- Conceptual Contours: Each layer has different rates of change;
- Conceptual Dependency: The concepts in the upper layers only have meaning with the layers below, while the layers below can have meaning without that top layer.
And gives us also a few generic domain layer examples: Every action the system can do (Potential) should be in a lower layer. Everything that can trigger one of those actions (Operation) should be in a layer above it. The policy layer reflects business goals, laws and restrictions (top level business constrains). Everything that can decide, or help decide, what action should be triggered should be in a layer on top of the last one (Decision support). The commitment layer is both a policy layer because it reflects business goals, but it is also operational because those goals are ongoing and ephemeral. It reflects specific restrictions on Operations, for example specific contracts with customers.
These layers are quite generic, however that doesn’t mean they will fit all domains. Maybe all, some or none fits into your specific domain. We must find the most adequate layering to the system at hand, and evolve it as appropriate.
Nevertheless, whatever layering we decide upon, it must be simple and should have a maximum of 4 or 5 layers.
A knowledge level is a group of objects that describes how another group of objects should behave.
This pattern, “knowledge level”, comes down to a model abstraction. The example Eric Evans gives us is extremely clear:
- In a company we might have a structure of Department/Director/Vice-President, or module/manager/senior-manager, or yet a matrix structure each person reports to different managers according to different purposes. Although these structures represent a single concept, they have different data and behaviour. If we need our software to be able to handle different representations of the same concept, we need to abstract them
The Knowledge Level pattern provides that structural abstraction. This will help avoiding classes who are “jack of all trades” and following the SRP and all the maintainability benefits it brings us.
Pluggable component framework
When designing a project we should segregate logic, on a first level, by defining the business concepts which will be, in fact, our bounded contexts. These bounded contexts must be made explicit in the project structure as modules/components.
These project components must be decoupled from each other, so that they can be replaced at any given time, maybe even at run time. One way to achieve this is to connect the components through interfaces, which should be defined in an abstract core.
An Abstract Core is simply a set of interfaces that define responsibilities and functionality of the application components. This Abstract Core, the set of interfaces, should be defined in a “central hub”, external to all components used in the project.
The problem with this approach is that we need to define the interfaces before hand, and this involves a deep knowledge and understanding of the domain from the start. However, the DDD approach is based on continuous iteration, refinement and evolving understanding of the domain.
Personally, I have successfully used a variant of this approach. The approach was based on an evolutive architecture, and “need to do” mindset. The team would define the interfaces in the components themselves, as opposed to a shared Abstract core. The interfaces would be refined as needed, in an evolutive approach. If and when we would need to swap the implementation of the component, we would extract its interface to the shared Abstract Core and have the new component comply to that interface.
Of course, we could do this because we owned all of the components. If we would need for 3rd party components to indiscriminately plug-in to our application, we would probably need to have an abstract core from the start.
Eric Evans gives us the example of a machinery software vendor, which created a framework to allow machinery from other vendors to work together. A client company would set up their framework and attach different machinery to the framework. The only requisite was that the machinery vendors would implement the required interfaces into the machinery software.