Smart use of domain modeling by software developers and architects can make complex applications more scalable — Best practice from the Alibaba Tech Team
Domain modeling is one of two basic approaches to application design. On a basic level, domain modeling can be understood by taking “domain” to mean the sum total of the knowledge of the business and “modeling” to mean an object-oriented abstraction of the business logic. After constructing a domain model consisting of different objects based on the business requirements, the architect then writes code to carry out the business logic between the objects.
The other basic approach is transaction script. Simple, straightforward, and easy to use, it conceives of business logic as a series of procedures and sub-procedures. (Beyond this, there are hybrid approaches such as command query responsibility segregation (CQRS), which is based on the notion of using different models for reading and updating data).
No one-size-fits-all solution exists among these options. The most suitable approach depends on the complexity of the business requirements. For example, using the domain model for a query-and-report process is unnecessarily complicated. In cases like this, it helps to recognize simplicity for what it is, get rid of the domain layers, and gain direct access to the infrastructure layer.
However, just as defaulting to a domain modeling approach risks over-engineering in simple cases, attempting to approach inherently complex business scenarios using a simple transaction script is also a mistake. Transaction script tends to make a mess of the code when dealing with complex scenarios, resulting in an exponential increase in the corrosion and complexity of the system as the application develops.
Domain modeling offers the option to build more robust, scalable, and user-friendly applications when dealing with complex business logic. The issue, then, is identifying when to use this approach, and learning how to apply it effectively.
Understanding the Benefits of Domain Modeling
The object-based nature of domain modeling can help the architect govern the development of an application more easily. It insists on the cohesiveness and reusability of objects, and encapsulates the business logic more intuitively.
This can be demonstrated with a use case. Consider the following example of a bank transfer carried out based variously on transaction script and domain modeling. This is a classic example often used to compare these two approaches, for example in this blog by Lorenzo Dee.
Bank transfer with transaction script
In following example, the business logic of a money transfer between two bank accounts is written in the implementation of MoneyTransferService, where Account is only the data structures of getters and setters. This is what we call an anemic domain model.
If this code looks familiar, it is because most existing systems are written in this way. Typically, the architect would start with a requirements review, draw some unified modeling language (UML) diagrams to conclude the design, and then start coding as above. The architect doesn’t have much to think about beyond the end goal of the code.
On the surface, this looks like an easy — even trivial — approach capable of successfully implementing the required function. This is why many developers perceive writing business code as a tedious menial task. But it needn’t be this way. Transaction script is far from an ideal approach in this case and many others, because it offers little beyond a quick fix to the problem at hand.
Domain modeling, on the other hand, is a far more holistic approach, and when used properly it results in much more extensible, maintainable code. Though typically thought of as a domain-level approach, domain modeling can also be employed at the infrastructure level too.
The other advantage for application developers is that a domain-driven development (DDD) approach — far from being tedious and mundane — requires the developer to exercise their powers of abstraction and modeling skills. These are essential skills that must be developed in order to progress to an application architect, who works at the domain level where the business is typically much more complex.
Bank transfer using domain modeling
Using ubiquitous language allows an architect using a domain modeling approach to make the implicit business logic more explicit, making it possible to govern the complexity. Additionally, if using domain-driven development (DDD), the entity Account contains behaviors and business logic such as methods of debit ( ) and credit ( ) in addition to account attributes.
The OverdraftPolicy is also abstracted from an Enum into an object of its own with business rules and strategy pattern.
Meanwhile, the domain service only needs to call domain objects to complete the business logic.
This DDD refactoring scatters the logic in the transaction script to three defined objects: domain service, domain entity, and Overdraftpolicy.
To summarize, the domain modeling approach offers benefits over transaction script for complex business scenarios because it is object-oriented and makes business semantics explicit.
All Account-related operations are encapsulated in the Account entity with a higher level of cohesiveness and reusability.
The OverdraftPolicy adopts a strategy pattern, which is a typical application of the polymorphism and improves the scalability of the code.
Explicit business semantics:
· Ubiquitous language
Having a ubiquitous language shared among the development team is advantageous for communication, coding, drafting, writing, and speaking. It is best practice to ensure that all important domain concepts, such as account, transfer, and overdraftpolicy, have a consistent name that is used in all contexts from day-to-day discussions to the product requirements document (PRD). This can significantly improve code readability and cut cognitive load.
· Explicit logic
Domain modeling extracts the implicit business logic from what would be a stack of ‘if-else’ statements in transaction script and uses a ubiquitous language to name, code, and extend this logic, turning it into an explicit concept.
For example, the meaning of ‘overdraftpolicy’ written in transaction script is completely buried in the code logic, leaving an uninitiated reader rather confused. Domain modeling, on the other hand, abstracts this logic using a strategy pattern to ensure better readability and scalability.
Domain Modeling Basics
Faced with the huge quantity of works available on the subject of domain modeling — and tedious methodologies such as in-depth syntactic analysis — getting started can seem daunting.
Instead of delving into domain modeling theory, the Alibaba tech team have found that it helps to remember two basic principles:
· Know your domain
Building a good model starts with having a solid understanding of the business. Without this, syntactic analysis alone will not produce a good model.
· Start from the basics and elaborate later
While it is important to learn as much about your domain as possible, sometimes the architect needs to start modeling based on an incomplete knowledge. In these cases, it makes sense to start with the basics, build a simple model, and adjust it later as required.
With these in mind, it makes sense to take an iterative approach to domain modeling. First, grasp some core concepts before writing the code and running it. If it goes well, there is no need to adjust it. If there are problems, the next step is to make adjustments to the model. As the architect’s understanding of the business builds, the iterations continue.
The first iteration: building a simple model:
Building a basic model starts with extracting the noun and verbs from the user story to identify the key objects, attributes, patterns, and relationships.
Let’s explore how this would work with the example of a job agency. A typical user story would be as follows: “Tom is looking for a job through an agency and the agency asks him to leave his number so he can be informed about any job opportunities”.
The key nouns in this story are most likely the domain objects we need:
· Tom is the jobseeker
· Phone number is the jobseeker’s attribute
· Agency refers to two key objects — the company and its employee
· Job opportunity is another key domain object
The verb inform indicates that the Observer pattern would be most appropriate in this case.
Now, let’s consider the relationships among these domain objects. There is a many-to-many (M2M) relationship between jobseekers and job opportunities:
· A jobseeker can have multiple job opportunities
· A job opportunity can be applied for by multiple jobseekers
Meanwhile, there is a one-to-many (O2M) relationship between agency and employee, since the agency can employ multiple employees.
This model is now nearly complete, but many real business scenarios call for far more complexity than this in the first iteration. For example, not all nouns are domain objects — they may be attributes as well. That is why solutions must be developed on a case-by-case basis, with a solid understanding of the business. Doing this successfully requires strong abstraction abilities and plenty of modeling experience.
For example, “price” and “storage” are typically attributes of “orders” and “goods.” However, in complex business scenarios price calculations and inventory deductions can be extremely complex. In Alibaba’s e-commerce business, they are so complex that price and inventory each constitute an entire domain.
That said, modeling is rarely a one-time effort, and an architect’s picture of the system becomes more comprehensive as their understanding of the business evolves. Iterations and refactoring are an inevitable part of the modeling process.
Subsequent iterations: model unification and evolution
After building a basic model in the first iteration, subsequent work will normally focus on the unification and evolution of the model. This means improving the cohesiveness of the model and expanding it to accommodate evolving business requirements.
In the parable of the blind men and an elephant, several blind men come to different conclusions of what an elephant is — a snake, tree trunk, or fan — based on what they have learnt by touching its trunk, legs or ears. Similarly, different architects will have different perceptions of the same business depending on their level of experience and knowledge.
Unifying a domain model involves integrating complementary abstractions to refine the model while removing inaccurate or mistaken abstractions. To revisit the example of the parable, two blind men may touch an elephant’s trunk and separately conclude that it is a snake and a fire hose. Each of these abstractions identify different attributes of the trunk — just like the elephant’s trunk, a snake is alive and moving, while a fire hose sprays water — but neither is complete. A new abstraction is needed that combines all of these attributes.
Meanwhile, the new abstraction must also exclude any incorrect attributes and behaviors that came with the original abstractions, such as fangs (snake) or the need to be rolled up and stored on the fire engine (fire hose).
Businesses are constantly changing, meaning that model elaboration is an ongoing process. As a business grows in scope and complexity, one challenge for the architect is to keep their understanding up-to-date and amend the model accordingly.
Meanwhile, the architect’s understanding of the existing business is constantly growing. This raises the possibility of carrying out code refactoring each time they have a breakthrough in understanding. Often, a series of quick changes can result in a much more practical model that is better suited to users’ needs.
Keeping up with an evolving business and one’s own evolving understanding takes both confidence and competence. Confidence is required to commit to refactoring a project on a tight schedule, while competence is required to ensure the refactoring does not undermine the existing business logic. Therefore, continuous integration (CI) is a must-have for evolution.
We can explore the idea of model evolution further by revisiting the bank transfer example from earlier. Suppose the bank’s business evolves to support different transfer channels — cash, credit card, mobile payment, Bitcoin, and others — each with its own constraints. Suppose that it also evolves to support transfers from a single debit account to multiple credit accounts. In this case, it would no longer be appropriate to use only one transfer (from Account, to Account). A better approach would be to abstract a specific domain object “Transaction” to better reflect the business logic.
This evolution process is illustrated in the following figure.
Understanding domain services:
The bank transaction example above raises the important but tricky concept of domain services.
Simply put, some actions in the domain are verbs, yet they do not belong to any object. They represent an important action in the domain that can neither be neglected nor incorporated into another entity or value object. When such an action is identified in the domain, the best practice is to declare it as a service. Services do not have a built-in status, and do no more than provide relevant functions for the domain.
A service is typically named after an activity instead of an entity. In the case of the bank transfer, the action of “transfer” is an important domain concept; however, it does not belong to any account entity because it occurs between two accounts. There is no need to associate an account entity with the account entity to which the money is transferred. In this case, using MoneyTransferDomainService is the best approach.
To summarize, to qualify as a domain service a concept must satisfy the following three criteria:
1. The operation represents a domain concept which does not naturally belong to any entity or value object.
2. The operation executed involves other objects in the domain.
3. The operation is stateless.
Domain services and the domain layer:
A system generally has three main layers, namely the application layer, the domain layer, and the infrastructure layer. Services exist on the application and domain layers. This raises the question of which services should exist on which layers.
A helpful rule to consider when making this decision is:
· If the operation belongs to the application layer in principle, it should be placed on this layer.
· If the operation involves domain objects and provides service for the domain, then it belongs to that layer.
In other words, any actions involving important domain concepts should be placed on the domain layer. Other technical code that does not concern domain logic should be placed on the application layer. This might include parameter resolving, context assembly, domain service calling, messaging, etc.
The following figure suggests how to partition services into layers in the case of a bank transfer:
Supporting Business Visualization and Configuration
One final thing to note is that a good domain model reduces the complexity of the application and can therefore support elegant visualization and configuration. This helps stakeholders gain a straightforward understanding of and learn how to configure the system — especially non-technical staff and clients. Ultimately, this offers a code-free solution, which is the main selling point of software as a service (SaaS).
However, visualization and configuration inevitably also introduce extra complexity to the system, so architects are advised to exercise caution when using them. It is best practice to reduce the coupling between the business logic of visualization and configuration and the business logic of the business itself to a minimum. Otherwise, it may undermine the existing architecture and make things even more complicated.
(Original article by Zhang Jianfei张建飞)
First-hand and in-depth information about Alibaba’s latest technology → Search “Alibaba Tech” on Facebook