Book Review: Domain-Driven Design Quickly

This is my summary of the book Domain Driven Design Quickly, I’ve highlighted the main concepts as a reference.

Model Driven Design
Model-Driven Design – Eric Evans

Entities
  • There is a category of objects which seem to have an identity, which remains the same throughout the states of the software. For these objects it is not the attributes which matter, but a thread of continuity and identity, which spans the life of a system and can extend beyond it. Such objects are called Entities.
  • Entities are important objects of a domain model, and they should be considered from the beginning of the modelling process. It is also important to determine if an object needs to be an entity or not, which is discussed in the next pattern.
Value Object
  • There are cases when we need to contain some attributes of a domain element. We are not interested in which object it is, but what attributes it has. An object that is used to describe certain aspects of a domain, and which does not have identity, is named Value Object.
  • Having no identity, Value Objects can be easily created and discarded. Nobody cares about creating an identity, and the garbage collector takes care of the object when is no longer referenced by any other object. This simplifies the design a lot.
  • It is highly recommended that value objects be immutable. They are created with a constructor, and never modified during their life time.
  • Being immutable, and having no identity, Value Objects can be shared.
  • They also manifest integrity, i.e. data integrity.
  • Value Objects should be kept thin and simple.
  • Value Objects can contain other Value Objects, and they can even contain references to Entities.
Services
  • When we develop the ubiquitous language, the key concepts of the domain are introduced in the language, and the nouns of the language are easily mapped to objects.
  • The verbs of the language, associated with their corresponding nouns become the part of the behaviour of those objects.
  • But there are some actions in the domain, some verbs, which do not seem to belong to any object. They represent an important behaviour of the domain, so they cannot be neglected or simply incorporated into some of the Entities or Value Objects. Adding such behaviour to an object would spoil the object, making it stand for functionality which does not belong to it.
  • Such an object does not have an internal state, and its purpose is to simply provide functionality for the domain.
  • A Service should not replace the operation which normally belongs on domain objects. We should not create a Service for every operation needed.
  • There are three characteristics of a Service:
    1. The operation performed by the Service refers to a domain concept which does not naturally belong to an Entity or Value Object.
    2. The operation performed refers to other objects in the domain.
    3. The operation is stateless.
Modules
  • For a large and complex application, the model tends to grow bigger and bigger. The model reaches a point where it is hard to talk about as a whole, and understanding the relationships and interactions between different parts becomes difficult. For that reason, it is necessary to organise the model into modules.
  • Modules are used as a method of organising related concepts and tasks in order to reduce complexity.
  • It is widely accepted that software code should have a high level of cohesion and a low level of coupling. While cohesion starts at the class and method level, it can be applied at module level. It is recommended to group highly related classes into modules to provide maximum cohesion possible.
  • Communicational cohesion is achieved when parts of the module operate on the same data. It makes sense to group them, because there is a strong relationship between them.
  • The functional cohesion is achieved when all parts of the module work together to perform a well-defined task. This is considered the best type of cohesion.
  • Instead of calling three objects of a module, it is better to access one interface, because it reduces coupling. Low coupling reduces complexity, and increases maintainability. It is easier to understand how a system functions when there are few connections between modules which perform well defined tasks, than when every module has lots of connections to all the other modules.
  • Give the Modules names that become part of the Ubiquitous Language. Modules and their names should reflect insight into the domain.
Aggregates
  • Aggregate is a domain pattern used to define object ownership and boundaries.
  • The number of associations should be reduced as much as possible.
  • An Aggregate is a group of associated objects which are considered as one unit with regard to data changes. The Aggregate is demarcated by a boundary which separates the objects inside from those outside.
  • Each Aggregate has one root. The root is an Entity, and it is the only object accessible from outside. The root can hold references to any of the aggregate objects, and the other objects can hold references to each other, but an outside object can hold references only to the root object.
  • If there are other Entities inside the boundary, the identity of those entities is local, making sense only inside the aggregate.
  • If objects of an Aggregate are stored in a database, only the root should be obtainable through queries. The other objects should be obtained through traversal associations.
Factories
  • In fact trying to construct a complex aggregate in its constructure is in contradiction with what often happens in the domain itself, where things are created by other things.
  • When a client object wants to create another object, it calls its constructor and possibly passes some parameters. But when the object construction is a laborious process, creating the object involves a lot of knowledge about the internal structure of the object, about the relationships between the objects contained, and the rules applied to them. This means that each client of the object will hold specific knowledge about the object built. This breaks encapsulation of the domain objects and of the Aggregates.
  • Creation of an object can be a major operation in itself, but complex assembly operations do not fit the responsibility of the created objects. Combining such responsibilities can produce ungainly designs that are hard to understand.
  • There are times when a Factory is not needed, and a simple constructor is enough. Use a constructor when:
    • The construction is not complicated.
    • The creation of an object does not involve the creation of others, and all the attributes needed are passed via the constructor.
    • The client is interested in the implementation, perhaps wants to choose the Strategy used.
    • The class is the type. There is no hierarchy involved, so no need to choose between a list of concrete implementations.
Repositories
  • use a Repository, the purpose of which is to encapsulate all the logic needed to obtain object references. The domain objects won’t have to deal with the infrastructure to get the needed references to other objects of the domain. They will just get them from the Repository and the model is regaining its clarity and focus.
  • Repository should have a set of methods used to retrieve objects.
  • We should not mix a Repository with a Factory. The Factory should create new objects, while the Repository should find already created objects. When a new object is to be added to the Repository, it should be created first using the Factory, and then it should be given to the Repository which will store it.
Refactoring
  • Designing without a model can lead to software which is not true to the domain it serves, and may not have the expected behaviour.
  • Modelling without feedback from the design and without developers being involved leads to a model which is not well understood by those who have to implement it.
  • There are many ways to do code refactoring. There are even refactoring patterns. Such patterns represent an automated approach to refactoring. There are tools built on such patterns making the developer’s life much easier than it used to be.
  • The nouns are converted to classes, while the verbs become methods. This is a simplification, and will lead to a shallow model. All models are lacking depth in the beginning, but we should refactor the model toward deeper and deeper insight.
  • When we talk to the domain experts, we exchange a lot of ideas and knowledge. Some of the concepts make their way into the Ubiquitous Language, but some remain unnoticed at the beginning.
  • Implicit concepts should not stay that way. If they are domain concepts, they should be present in the model and the design.
  • Processes are usually expressed in code with procedures. We won’t use a procedural approach, since we are using an object-oriented language, so we need to choose an object for the process, and add a behaviour to it. The best way to implement processes is to use a Service. If there are different ways to carry out the process, then we can encapsulate the algorithm in an object and use a Strategy.
  • The last method to make concepts explicit that we are addressing here is Specification. Simply said, a Specification is used to test an object to see if it satisfies a certain criteria.
Model Integrity
  • It is so easy to start from a good model and progress toward an inconsistent one.
  • The first requirement of a model is to be consistent, with invariable terms and no contradictions.
  • The internal consistency of a model is called unification.
Bounded Context
Domain-Driven Design – Eric Evans
Bounded Context
  • Each model has a context. When we deal with a single model, the context is implicit.
  • But when we work on a large enterprise application, we need to define the context for each model we create.
  • There is no formula to divide one large model into smaller ones.
  • A model should be small enough to be assigned to one team.
  • The main idea is to define the scope of a model, to draw up the boundaries of its context, then do the most possible to keep the model unified.
  • It is hard to keep a model pure when it spans the entire enterprise project, but it is much easier when it is limited to a specified area.
  • Explicitly define the context within which a
  • model applies. Explicitly set boundaries in terms of team organisation, usage within specific parts of the application, and physical manifestations such as code bases and database schemas.
  • A Bounded Context is not a Module. A Bounded Context provides the logical frame inside of which the model evolves. Modules are used to organise the elements of a model, so Bounded Context encompasses the Module.
  • When using multiple models, everybody can work freely on their own piece. We all know the limits of our model, and stay inside the borders. We just have to make sure we keep the model pure, consistent and unified. Each model can support refactoring much easier, without repercussions on other models.
  • There is a price to pay for having multiple models. We need to define the borders and the relationships between different models. This requires extra work and design effort, and there will be perhaps some translation between different models. We won’t be able to transfer any objects between different models, and we cannot invoke behavior freely as if there was no boundary. But this is not a very difficult task, and the benefits are worth taking the trouble.
  • The recommended approach is to create a separate model for each of the domains. They can both evolve freely without much concern about each other, and even become separate applications.
Continuous Integration
  • If one does not understand the relationships between objects, they may modify the code in such a way that comes in contradiction with the original intent. It is easy to make such a mistake when we do not keep 100% focus on the purity of the model.
  • We need a process of integration to make sure that all the new elements which are added fit harmoniously into the rest of the model, and are implemented correctly in code.
  • Continuous Integration applies to a Bounded Context, it is not used to deal with relationships between neighbouring Contexts.
Context Map
  • A Context Map is a document which outlines the different Bounded Contexts and the relationships between them.
  • A Context Map can be a diagram like the one below, or it can be any written document. The level of detail may vary.
  • What it is important is that everyone working on the project shares and understands it.
  • If the relationships between contexts are not outlined, there is a chance they won’t work when the system is integrated.
  • Each Bounded Context should have a name which should be part of the Ubiquitous Language.
  • The Shared Kernel and Customer-Supplier are patterns with a high degree of interaction between contexts. Separate Ways is a pattern used when we want the contexts to be highly independent and evolve separately. There are another two patterns dealing with the interaction between a system and a legacy system or an external one, and they are Open Host Services and Anticorruption Layers.
Shared Kernel
  • The purpose of the Shared Kernel is to reduce duplication, but still keep two separate contexts.
  • Development on a Shared Kernel needs a lot of care. Both teams may modify the kernel code, and they have to integrate the changes.
  • If the teams use separate copies of the kernel code, they have to merge the code as soon as possible, at least weekly.
  • A test suite should be in place, so every change done to the kernel to be tested right away.
  • Any change of the kernel should be communicated to another team, and the teams should be informed, making them aware of the new functionality.
Customer-Supplier
  • There are times when two subsystems have a special relationship: one depends a lot on the other. The contexts in which those two subsystems exist are different, and the processing result of one system is fed into the other.
  • There might be some overlapping, but not enough to justify a Shared Kernel.
  • A Customer-Supplier relationship is viable when both teams are interested in the relationship. The customer is very dependent on the supplier, while the supplier is not.
  • When two development teams have a Customer-Supplier relationship in which the supplier team has no motivation to provide for the customer team’s needs, the customer team is helpless. The most obvious one is to separate from the supplier and to be completely on their own. We will look at this later in the pattern Separate Ways.
  • But because the supplier team does not help the customer team, the latter has to take some measures to protect itself from model changes performed by the former team. They will have to implement a translation layer which connects the two contexts.
  • The customer context can still make use of it, but it should protect itself by using an Anticorruption Layer which we will discuss later.
  • The customer team cannot make changes to the kernel. They can only use it as part of their model, and they can build on the existing code provided.
Anticorruption Layer
  • We can’t ignore the interaction with the external model, but we should be careful to isolate our own model from it.
  • But the Anticorruption Layer talks to the external model using the external language not the client one.
  • This layer works as a two way translator between two domains and languages.
  • The greatest achievement is that the client model remains pure and consistent without being contaminated by the external one.
  • A very good solution is to see the layer as a Service from the client model. It is very simple to use a Service because it abstracts the other system and let us address it in our own terms. The Service will do the needed translation, so our model remains insulated.
  • The Anticorruption Layer may contain more than one Service.
  • For each Service there is a corresponding Façade, and for each Façade we add an Adapter. We should not use a single Adapter for all Services, because we clutter it with mixed functionality.
  • The Adapter takes care of wrapping up the behaviour of the external system. We also need object and data conversion. This is done using a translator. This can be a very simple object, with little functionality, serving the basic need of data translation.
Separate Ways
  • It’s one thing to develop independently, to choose the concepts and associations freely, and another thing to make sure that your model fits into the framework of another system.
  • We may need to alter the model just to make it work with the other subsystem. Or we may need to introduce special layers which perform translations between the two subsystems. There are times when we have to do that, but there are times when we can go a different path.
  • We should look at the requirements and see if they can be divided in two or more sets which do not have much in common. If that can be done, then we can create separate Bounded Contexts and do the modelling independently.
  • Before going on Separate Ways we need to make sure that we won’t be coming back to an integrated system. Models developed independently are very difficult to integrate. They have so little in common that it is just not worth doing it.
Open Host Service
  • When we try to integrate two subsystems, we usually create a translation layer between them.
  • If the external subsystem turns out to be used not by one client subsystem, but by several ones, we need to create translation layers for all of them. All those layers will repeat the same translation task, and will contain similar code.
  • The solution is to see the external subsystem as a provider of services. If we can wrap a set of Services around it, then all the other subsystems will access these Services, and we won’t need any translation layer.
  • The difficulty is that each subsystem may need to interact in a specific way with the external subsystem, and to create a coherent set of Services may be problematic. Then, use a one-off translator to augment the protocol for that special case so that the shared protocol can stay simple and coherent.
Distillation
  • Distillation is the process of separating the substances composing a mixture.
  • The idea is to define a Core Domain which represents the essence of the domain.
  • The byproducts of the distillation process will be Generic Subdomains which will comprise the other parts of the domain.
  • The Core Domain of a system depends on how we look at the system.
  • Boil the model down. Find the Core Domain and provide a means of easily distinguishing it from the mass of supporting model and code. Emphasize the most valuable and specialised concepts. Make the Core small.
  • Spend the effort in the Core to find a deep model and develop a supple design—sufficient to fulfil the vision of the system.
  • Apply your top talent to the Core Domain, and recruit accordingly.
  • There is a process of refinement and successive refactorings are necessary before the Core emerges more clearly.
  • There are different ways to implement a Generic Subdomain:
    • Off-the-shelf Solution.
    • Outsourcing.
    • Existing Model.
    • In-House Implementation.