There is a lot of talk about clean code and architecture nowadays. There is more and more talk about how to achieve it. The rules described by Robert C. Martin are universal and in my opinion, we can use them in various other contexts.
In this post I would like to refer them to the context of the Domain Model implementation, which is often the heart of our system. We want to have a clean heart, aren’t we?
As we know from the Domain-Driven Design approach, we have two spaces – problem space and solution space. Vaughn Vernon defines these spaces in DDD Distilled book as follows:
Problem space is where you perform high-level strategic analysis and design steps within the constraints of a given project.
Solution space is where you actually implement the solution that your problem space discussions identify as your Core Domain.
So the implementation of the Domain Model is a solution to a problem. To talk about whether the Domain Model is clean, we must define what a clean solution means first.
What is Clean Solution
I think that everyone has a definition of what a clean solution means or at least feels if what he does is clean or not. In any case, let me describe what it means to me.
First of all – a given solution has to solve a defined problem. This is mandatory. Even most-elegant solution which doesn’t solve our problem is worth nothing (in context of problem).
Secondly, a clean solution should be optimal from the point of view of time. It should take no more or less time than necessary.
Thirdly, it should solve exactly one defined problem with simplicity in mind but with adaption to change. We should keep always the balance – no less (no design or under-design), no more (over-design, see YAGNI). This could be summarized by a quote from Antoine Airman’s Odyssey book:
Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.
Note: Of course we should strive for perfection by being aware that we will never achieve it.
Fourthly, we should solve problems based on the experience of others and learn from their mistakes. This is why architectural and design patterns or principles are so useful. We can take someone’s solution and adapt it to our needs. Moreover, we can take tools created by others and use them. This is game-changer in the IT world.
Last, but not least attribute is the level of understanding of the solution. We should be able to understand our solution even after a lot of time. More importantly, others should be able to understand it. The code is much more read than written. Martin Fowler said:
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.
This is one of my favorite IT quotes. I try to remember it constantly as I write the code.
To summarize, the clean solution should solve exactly our problem in optimal time with a good balance of simplicity vs complexity. It should be developed based on other experiences, common knowledge, using appropriate tools and should be understandable. Considering all these factors, how can we determine that our Domain Model is a clean solution?
Clean Domain Model
1. Uses Ubiquitous Language
Ubiquitous Language which is one of the fundamentals of DDD strategic patterns in Domain Model plays key role. I like to think about writing code in a Domain Model like writing a book for business. Business people should be able to understand all logic, terms, and concepts which are used – even this is a computer program and they don’t know programming language! If we use the same language, concepts and meaning everywhere in a defined context, understanding of our problems and solutions can only increase the probability of success of our project.
2. Is Rich in Behavior
Domain Model, as the name suggests, models the domain. We have behavior in the domain, so our model should also have it. If this behavior is absent, we have dealing with the so-called Anemic Domain Model anti-pattern. For me, this is really a Data Model, not model of domain. This kind of model is useful sometimes but not when we have complex behvior and logic. It makes our code procedural and all the benefits of object-oriented programming disappear. The real Domain Model is rich in behavior.
3. Is encapsulated
Encapsulation of Domain Model is very important. We want to expose only the minimum information about our model to the outside world. We should prevent leaking business logic to our application logic or even worse – GUI. Ideally, we should expose only public methods on our aggregates (only entry to Domain Model). More information how achieve good level of Domain Model encapsulation you can read in my eariler post Domain Model Encapsulation and PI with Entity Framework 2.2.
4. Is Persistence Ignorant
Persistence Ignorance (PI) is another well-known concept and desirable attribute of Domain Model. We don’t want to have high coupling between our model and the persistence store. These are different concepts, tasks and responsibilities. Change in one thing should have minimal impact on other and vice-versa. Low coupling in this area means greater adaption of our system to change. Again, you can read how to minimize this coupling in .NET world using EF Core in the post which is linked above.
5. Sticks to SOLID Principles
As the Domain Model is object-oriented, all SOLID principles should be followed. Examples:
- SRP – one Entity represents one business concept. One method does one thing. One event represents one fact.
- O/C – business logic implementation should be easy to extend without changing other places. For that case, Strategy/Policy Pattern is most often used.
- LSP – because of sticking to the composition over inheritance rule, sticking to LSP rule should be easy. Inheritance is the strongest level of coupling between classes and this is what we want to avoid
- ISP – interfaces of our Domain Services or policies should be small – ideally should have one method
- DI – to decouple our Domain Model from the rest of the application and infrastructure we need to use the Dependency Injection and Dependency Inversion principle. This combination gives us a powerful weapon – we can keep the same level of abstraction in our model, use business language in the whole model and hide implementation details from him.
6. Uses Domain Primitives
Most of the codebases I see operate on primitive types – strings, ints, decimals, guids. In the Domain Model, this level of abstraction is definitely too low.
We should always try to express significant business concepts using classes (entities or value objects). In this way, our model is more readable, easier to maintain, rich in behavior. In addition, our code is more secure because we can add validation at the class level. More about Domain Primitives you can read here.
7. Is efficient
Even the best-written Domain Model that cannot handle a request in a satisfactory time is useless. That is why entity size and aggregate boundaries are so important.
When designing aggregates, you need to know how they will be processed – how often, how many users will try to use them at the same time, whether there will be periods of increased activity and so on. The smaller the aggregate, the shorter data needs to be loaded and saved and the transaction is shorter. On the other hand, the smaller the aggregate, the smaller the consistency boundary so the correct balance is needed.
8. Is expressive
Class structure in Object-Oriented languages is a powerful tool to model business concepts. Unfortunately, it is often used incorrectly. The most common mistake is modeling many concepts with one class. For this (often unconsciously) statuses and bit flags are used.
Most often, such a class can be divided into several classes, which supports SRP. A good heuristics here are looking at the business language and looking at our entity’s behavior.
9. Is testable and tested
Domain Models are used to solve complicated problems. A complicated problem means complicated business logic to solve it. If you have a complicated solution you have to be sure that it works correctly and you can’t be afraid of changing this logic.
This is where unit tests enter, which are an integral part of a clean Domain Model. Clean Domain Model is testable and should have a maximum test coverage factor. This is the heart of our application and we should always be one hundred percent sure that it works as we expected.
10. It should be written with ease
This point is a summary of all the above. If writing your Domain Model code is a problem every time – it’s hard to change it, understand it, test it, use it – something is wrong. What you should do?
Review all the points described by me above and see what your model does not meet? Maybe it depends on the infrastructure? Maybe he speaks a non-business language? Maybe it’s anemic, poorly encapsulated or untestable? If your model meets all attributes described in this post, then solving business problems should be easy and pleasant. I assure you.
So I have a question – today are you relaxed by writing your business logic or did you hit your head against the wall again? Is your Domain Model clean?