The author | YanHao

New retail product | alibaba tao technology

Takeaway: Reducing system complexity in software development has always been a challenge for an architect, whether it was GoF’s Design Patterns in 1994, Martin Fowler’s Refactoring in 1999, P of EAA in 2002, Enterprise Integration Patterns, back in 2003, reduce common complexity through a series of design Patterns or paradigms. But the problem is that the idea of these books is to solve technical problems through technical means, but they don’t solve business problems at all. Therefore, Eric Evans’ book Domain Driven Design in 2003, Vaughn Vernon’s Implementing DDD, Uncle Bob’s Clean Architecture and other subsequent books, Truly from the business perspective, for the vast majority of the world to do pure business development to provide a set of architectural ideas.

preface

Because DDD is not a set of framework, but an architectural idea, there is a lack of sufficient constraints at the code level, resulting in a high threshold for DDD in practical application, and even most people have a misunderstanding of DDD. For example, an anti-pattern, Anemic Domain Model, described by Martin Fowler in his personal blog, is emerging in practice, and some still-popular ORM tools such as Hibernate, Entity Framework actually encourages the proliferation of anemic models. Similarly, the traditional four-tier application architecture (UI, Business, Data Access and Database) based on Database technology and MVC is confused with some concepts of DDD to some extent, leading to the majority of people only use the idea of DDD modeling in practical applications. And its idea for the whole architecture system cannot be landed.

I got to know DDD for the first time in 2012. At that time, except for large Internet companies, almost all business applications were still in the era of single machine, and the service-oriented architecture was still limited to single machine +LB to provide Rest interface with MVC for external invocation. Or make RPC calls with SOAP or WebServices, but are really more limited to external dependencies on the protocol. What drew my attention to DDD thinking was a concept called the anti-corruption Layer, in particular its mechanism for isolating core business logic from external dependencies in the context of addressing frequent changes in external dependencies. By 2014, SOA had taken off, the concept of microservices was emerging, and how to split a Monolith application into microservices became a hot topic in the forums. The idea of Bounded Context in DDD provides a reasonable framework for microservice separation. Today, in an era where everything can be called a “service” (XAAS), the idea of DDD allows us to take a moment to think about what can be broken down as a service and what logic needs to be aggregated to bring minimal maintenance costs, rather than simply pursuing development efficiency.

So today, I’m starting this series of articles on DDD, hoping to continue to build on the work of our predecessors, but with a set of code structures, frameworks, and constraints that I think make sense to lower the bar for DDD practice and improve code quality, testability, security, and robustness.

Contents to be covered in the future include:

1. Best architectural practices: Core ideas and solutions of hexagonal application architecture/Clean architecture

2, continuous discovery and delivery: Event Storming > Context Map > Design Heuristics > Modelling

3. Reduce the rate of architecture Corruption: modular solution for integrating third-party libraries through anti-corruption Layer

4. Specification and boundary of standard components: Entity, Aggregate, Repository, Domain Service, Application Service, Event, DTO Assembler, etc

5. Redefine the boundaries of application services based on Use Case

6. Micro-service transformation and granularity control based on DDD

7. Transformation and challenge of CQRS architecture

8. Challenges of event-driven architecture

9, etc.

Today I’ll start with a very basic but valuable concept of Domain Primitive.

Domain Primitive

Just as the basic data types are the first thing you need to know when learning any language, before you get to the bottom of DDD, let me introduce you to a very basic concept: Domain Primitive (DP).

The definition of Primitive is:

It doesn’t develop from anything else

An early stage of formation or growth

Just as Integer and String are Primitive for all programming languages, DP is the basis for all models, methods and architectures in DDD, and just like Integer and String, DP is ubiquitous. So, the first lecture will be a comprehensive introduction and analysis of DP, but we will not go into the concept, but start with a case study to see why DP is a powerful concept.

Case analysis

Let’s start with a simple case where the business logic is as follows:

A new application is promoted in the whole country through the land push salesman. It needs to make a user registration system, and at the same time, it hopes to give bonus to the salesman through the region (area code) of the user’s phone (first assuming only landline) after the user registers.

First don’t go to tangle this according to the user phone to pay bonuses business logic is reasonable, also don’t go to the user should be in the registration and clerk binding, here we see the main or how to more reasonable to achieve this logic. A simple user and user registration code implementation is as follows:

public class User { Long userId; String name; String phone; String address; Long repId; } public class RegistrationServiceImpl implements RegistrationService { private SalesRepRepository salesRepRepo; private UserRepository userRepo; Public User Register (String Name, String phone, String Address) throws ValidationException {// Validate logicif (name == null || name.length() == 0) {
            throw new ValidationException("name");
        }
        if(phone == null || ! isValidPhoneNumber(phone)) { throw new ValidationException("phone"); SalesRep String areaCode = null; SalesRep String areaCode = null; String[] areas = new String[]{"0571"."021"."010"};
        for (int i = 0; i < phone.length(); i++) {
            String prefix = phone.substring(0, i);
            if (Arrays.asList(areas).contains(prefix)) {
                areaCode = prefix;
                break; } } SalesRep rep = salesRepRepo.findRep(areaCode); // Create User, drop disk, and return User User = new User(); user.name = name; user.phone = phone; user.address = address;if(rep ! = null) { user.repId = rep.repId; }return userRepo.save(user);
    }

    private boolean isValidPhoneNumber(String phone) {
        String pattern = "^ 0 [1-9] {2, 3} -? \\d{8}$";
        returnphone.matches(pattern); }}Copy the code

Most of our everyday code and models are similar to this, which at first glance seems fine, but let’s take it a step further and analyze it from four dimensions: interface clarity (readability), data validation and error handling, business logic code clarity, and testability.

Issue 1 – Interface clarity

In Java code, all argument names for a method are lost at compile time, leaving only a list of argument types, so let’s look at the interface definition above, which at runtime is just:

User register(String, String, String);
Copy the code

So the following code is a bug that the compiler does not error at all and is hard to spot by looking at the code:

service.register("YanHao".969 Wansan Xi Lu, Yuhang District, Hangzhou City, Zhejiang Province, China."0571-12345678");
Copy the code

Of course, errors are reported at run time in real code, but such bugs are found at run time, not compile time. Common Code Review is also difficult to find this problem, it is likely to be exposed after the Code is online. The thinking here is, is there a way to avoid this problem when coding?

Another common example, especially in query services, is as follows:

User findByName(String name);
User findByPhone(String phone);
User findByNameAndPhone(String name, String phone);
Copy the code

In this scenario, the input parameters are String, and the method name must be distinguished by ByXXX. FindByNameAndPhone also has the problem of incorrect input order, and unlike the previous input parameters, if the parameter order is wrong, the method name must be specified by ByXXX. The method returns null without an error, which is harder to spot. The thinking here is, is there a way to make method entries clear and avoid bugs caused by entry errors?

Problem 2 – Data validation and error handling

In the previous section of data verification code:

if(phone == null || ! isValidPhoneNumber(phone)) { throw new ValidationException("phone");
}
Copy the code

It often occurs in everyday coding, and generally this code needs to appear at the front of the method to ensure fail-fast. But if you have multiple similar interfaces and similar input arguments, this logic will be repeated in each method. More seriously, if we expand our numbers to include mobile phones in the future, we will probably need to include the following code:

if(phone == null || ! isValidPhoneNumber(phone) || ! isValidCellNumber(phone)) { throw new ValidationException("phone");
}
Copy the code

If you use phone in a lot of places, but forget to change one, it will cause a bug. This is a common problem when the DRY principle is violated.

This code becomes more complex if there is a new requirement to return the cause of the input error:

if (phone == null) {
    throw new ValidationException("Phone can't be empty.");
} else if(! isValidPhoneNumber(phone)) { throw new ValidationException("Phone format error");
}
Copy the code

As you can imagine, the maintenance costs can be high when the code is filled with a large number of similar code blocks.

Finally, in this business method, validationExceptions are thrown (implicit or explicit), so external callers need to try/catch, and business logic exceptions and data validation exceptions are mixed together. Is it reasonable?

In traditional Java architectures, there are several ways to solve some of these problems. Common examples are BeanValidation annotations or ValidationUtils classes, such as:

// Use Bean Validation
User registerWithBeanValidation(
  @NotNull @NotBlank String name,
  @NotNull @Pattern(regexp = "^ 0? [1-9] {2, 3} -? \\d{8}$") String phone, @NotNull String address ); // Use ValidationUtils: public User registerWithUtils(String name, String phone, String address) { ValidationUtils.validateName(name); // throws ValidationException ValidationUtils.validatePhone(phone); ValidationUtils.validateAddress(address); . }Copy the code

But these traditional methods also have problems,

BeanValidation:

Usually can only solve simple verification logic, complex verification logic also need to write code to implement custom verifier

When you add new validation logic, you will also forget to add an annotation somewhere, and the DRY principle will still be violated

ValidationUtils class:

When too much validation logic is concentrated in a Single class, the Single Responsibility principle of simplicity is violated, resulting in messy and unmaintainable code

Business exceptions and validation exceptions can still be mixed

So, is there a way to solve all validation problems once and for all and reduce subsequent maintenance costs and exception handling costs?

Issue 3 – Clarity of business code

In this code:

String areaCode = null;
String[] areas = new String[]{"0571"."021"."010"};
for (int i = 0; i < phone.length(); i++) {
    String prefix = phone.substring(0, i);
    if (Arrays.asList(areas).contains(prefix)) {
        areaCode = prefix;
        break;
    }
}
SalesRep rep = salesRepRepo.findRep(areaCode);
Copy the code

In fact, another common situation that occurs is to extract some data from some input, call an external dependency to get more data, and often extract some data from the new data for other purposes. This code, often referred to as “glue code,” is essentially the result of external dependent service inputs that do not match our original inputs. For example, if SalesRepRepository included a findRepByPhone method, most of the above code would be unnecessary.

So, a common approach is to separate this code out into one or more separate methods:

private static String findAreaCode(String phone) {
    for (int i = 0; i < phone.length(); i++) {
        String prefix = phone.substring(0, i);
        if (isAreaCode(prefix)) {
            returnprefix; }}return null;
}

private static boolean isAreaCode(String prefix) {
    String[] areas = new String[]{"0571"."021"};
    return Arrays.asList(areas).contains(prefix);
}
Copy the code

The original code then becomes:

String areaCode = findAreaCode(phone);
SalesRep rep = salesRepRepo.findRep(areaCode);
Copy the code

In order to reuse the above methods, a static utility class PhoneUtils might be removed. But the question here is, are static utility classes the best way to do this? When your project is littered with static utility classes and business code is scattered in multiple files, can you still find the core business logic?

Question 4 – Testability

To ensure code quality, every possible condition for every input parameter in every method needs to be covered by a TC (assuming we don’t test the internal business logic), so we need the following TC in our method:

If a method has N parameters and each parameter has M check logic, it must have at least N * M TC.

If fax is added to this method, M new TCS are required to ensure TC coverage even if the verification logic of FAX and phone is identical.

Suppose that the field phone is used in all P methods, and all P methods need to test this field, that is to say, the overall requirements are as follows:

** P * N * M **

It takes only two test cases to fully cover all data validation issues, and in everyday projects, the cost of this test is very high, resulting in a large amount of code not covered. Code that is not covered by tests is the most likely place for problems.

In this case, reducing the cost of testing == improving the quality of the code, how can you reduce the cost of testing?

The solution

Let’s go back and review the original use case and highlight concepts that may be important:

A new application is promoted in the whole country by the local salesman. It needs to make a user registration system. After the user registers, it can give bonus to the salesman through the area code of the user’s phone number.

After analyzing the use case, it is found that the local push salesman and user have their own ID attributes, which belong to Entity, while the registration system belongs to Application Service. These concepts already exist. But the concept of a phone number was completely hidden in the code. We can ask ourselves if the logic of getting the area code for the phone number belongs to the user (the user’s area code?). ? Is it a registration service (registered area code?) ? If none of this is quite right, then the logic should be a separate concept. So here’s our first principle:

**Make Implicit Concepts Explicit

Externalize the implicit concept **

Here, we can see that the original telephone number is only a parameter of the user, which is an invisible concept, but actually the area code of the phone number is the real business logic, and we need to make the concept of the telephone number explicit by writing a Value Object:

public class PhoneNumber {
  
    private final String number;
    public String getNumber() {
        return number;
    }

    public PhoneNumber(String number) {
        if (number == null) {
            throw new ValidationException("Number cannot be empty");
        } else if (isValid(number)) {
            throw new ValidationException("Number format error");
        }
        this.number = number;
    }

    public String getAreaCode() {
        for (int i = 0; i < number.length(); i++) {
            String prefix = number.substring(0, i);
            if (isAreaCode(prefix)) {
                returnprefix; }}return null;
    }

    private static boolean isAreaCode(String prefix) {
        String[] areas = new String[]{"0571"."021"."010"};
        return Arrays.asList(areas).contains(prefix);
    }

    public static boolean isValid(String number) {
        String pattern = "^ 0? [1-9] {2, 3} -? \\d{8}$";
        returnnumber.matches(pattern); }}Copy the code

There are several important elements to this:

1. Ensure that PhoneNumber is an Immutable Value Object by using private final String number. (Generally, VO is Immutable, but it is emphasized here.)

2. The validation logic is put into constructor to ensure that whenever class PhoneNumber is created, it passes.

3. The findAreaCode method is now getAreaCode for PhoneNumber, and areaCode is a calculated property of PhoneNumber.

After doing this, we see that by making PhoneNumber explicit, we actually generate a Type and a Class:

1. Type means that we can explicitly identify the concept of PhoneNumber through PhoneNumber in future code

2. Class means that we can collect all the logic related to the phone number into a complete file

Together, these two concepts form Domain Primitive (DP) under the title of this article.

Let’s take a look at the effect of using DP in full:

`public class User { UserId userId; Name name; PhoneNumber phone; Address address; RepId repId; }

public User register( @NotNull Name name, @NotNull PhoneNumber phone, @notnull Address Address) {// Find SalesRep SalesRep rep = Salesreprepo.findrep (phone.getareacode ());

User = new User(); User = new User(); user.name = name; user.phone = phone; user.address = address; if (rep ! = null) { user.repId = rep.repId; } return userRepo.saveUser(user); }Copy the code

We can see that with DP, all the data validation logic and non-business process logic are gone, leaving the core business logic, which can be seen at a glance. Let’s re-evaluate with the above four dimensions:

Evaluate 1 – interface clarity

The refactored method signature becomes clear:

public User register(Name, PhoneNumber, Address)
Copy the code

And the bugs that were easy to come up with before, if I write it this way

service.register(new Name("YanHao"), new Address(969 Wansan Xi Lu, Yuhang District, Hangzhou City, Zhejiang Province, China), new PhoneNumber("0571-12345678"));
Copy the code

Let the interface API become very clean, easy to expand.

Evaluation 2 – Data validation and error handling

public User register(
  @NotNull Name name,
  @NotNull PhoneNumber phone,
  @NotNull Address address
) // no throws
Copy the code

As shown in the previous code, the refactored method does not have any data validation logic at all and does not throw validationExceptions. This is because of the nature of DP that anything that can be carried to an input parameter must be correct or NULL (Bean Validation or Lombok annotations can solve the null problem). So we put the data validation work on the caller, who is supposed to provide legitimate data, so it’s more appropriate.

If you need to change PhoneNumber’s validation logic in the future, you only need to change it in a single file. Everything that uses PhoneNumber will take effect.

Assess 3 – clarity of business code

SalesRep rep = salesRepRepo.findRep(phone.getAreaCode());
User user = xxx;
return userRepo.save(user);
Copy the code

In addition to eliminating the need to validate data in business methods, the original glue code findAreaCode has been changed to getAreaCode, a calculation property of the PhoneNumber class, making the code much clearer. Glue code is usually not reusable, but with DP, it becomes reusable, testable code. We can see that after stripping out the data validation code, the glue code, all that is left is the core business logic. (Entity related refactoring will be covered in a later article and ignored for this time)

Evaluation 4 – Testability

After we extract PhoneNumber, let’s look at the TC of the test:

PhoneNumber itself still needs M test cases, but since we only need to test a single object, the amount of code per use case is significantly reduced and maintenance costs are reduced.

Each argument in each method now only needs to be overridden if it is null. Other cases cannot occur.

So, the TC of a single method goes from N * M to N + M today. Similarly, the number of TCS for multiple methods becomes zero

N + M + P

This quantity is generally much lower than the original quantity N* M * P, so that the test cost is greatly reduced.

Evaluation summary

Use the advanced

Above I introduced the first principle of DP: make implicit concepts explicit. Here I will introduce two more principles of DP, using a new case.

Case 1 – Transfer

Suppose we now want to implement A function that allows user A to pay x dollars to user B. The possible implementation is as follows:

public void pay(BigDecimal money, Long recipientId) {
    BankService.transfer(money, "CNY", recipientId);
}
Copy the code

If this is a domestic transfer and the domestic currency stays the same forever, there seems to be no problem with this method. However, if the currency changes one day (such as the problem in euro zone) or we need to do cross-border transfer, this method is obviously bug, because the currency corresponding to Money may not be CNY.

In this case, when we say “pay x dollars”, in addition to the number of x itself, there is actually an implicit concept of currency “yuan”. But in the original entry entry, we only used BigDecimal because we thought the CNY currency was the default and an implicit condition, but when we wrote the code, we needed to externalize all the implicit conditions that collectively make up the current context. So the second principle of DP is:

**Make Implicit Context Explicit

Make the implicit context explicit

So when we do this payment function, the one entry we actually need is the amount of payment + the currency of payment. We can combine these two concepts into a single, complete concept: Money.

@Value public class Money { private BigDecimal amount; private Currency currency; public Money(BigDecimal amount, Currency currency) { this.amount = amount; this.currency = currency; }}Copy the code

The original code becomes:

public void pay(Money money, Long recipientId) {
    BankService.transfer(money, recipientId);
}
Copy the code

By externalizing the implicit contextual concept of default Money and incorporating it with Money, we can avoid many bugs that are not obvious today but may explode in the future.

Case 2 – Cross-border transfer

The previous case can be upgraded, assuming that the user may want to make a cross-border transfer from CNY to USD, and the currency exchange rate fluctuates at any time:

public void pay(Money money, Currency targetCurrency, Long recipientId) {
    if (money.getCurrency().equals(targetCurrency)) {
        BankService.transfer(money, recipientId);
    } else{ BigDecimal rate = ExchangeService.getRate(money.getCurrency(), targetCurrency); BigDecimal targetAmount = money.getAmount().multiply(new BigDecimal(rate)); Money targetMoney = new Money(targetAmount, targetCurrency); BankService.transfer(targetMoney, recipientId); }}Copy the code

In this case, since the targetCurrency is not necessarily the same as money’s Curreny, a service needs to be called to fetch the exchange rate and then do the calculation. Finally, use the calculated results to make the transfer.

The biggest problem with this case is that the calculation of the amount is included in the payment service, and there are also 2 currencies, 2 Money, and 1 BigDecimal for a total of 5 objects. This kind of business logic involving multiple objects needs to be wrapped up in DP, so here comes the third principle of DP:

**Encapsulate Multi-Object Behavior

Encapsulate multi-object behavior **

In this case, the ExchangeRate function can be encapsulated in a DP called ExchangeRate:

@Value
public class ExchangeRate {
    private BigDecimal rate;
    private Currency from;
    private Currency to;

    public ExchangeRate(BigDecimal rate, Currency from, Currency to) {
        this.rate = rate;
        this.from = from;
        this.to = to;
    }

    public Money exchange(Money fromMoney) {
        notNull(fromMoney);
        isTrue(this.from.equals(fromMoney.getCurrency()));
        BigDecimal targetAmount = fromMoney.getAmount().multiply(rate);
        returnnew Money(targetAmount, to); }}Copy the code

ExchangeRate ExchangeRate object, which makes the original code extremely simple by encapsulating amount calculation logic and various verification logic:

public void pay(Money money, Currency targetCurrency, Long recipientId) {
    ExchangeRate rate = ExchangeService.getRate(money.getCurrency(), targetCurrency);
    Money targetMoney = rate.exchange(money);
    BankService.transfer(targetMoney, recipientId);
}
Copy the code

Discussion and conclusion

Definition of Domain Primitive

Let’s redefine Domain Primitive: Domain Primitive is a Value Object that has a well-defined, self-verifiable behavior in a specific Domain.

1. DP is a Value Object in the traditional sense, which is Immutable

2. DP is a complete conceptual whole with precise definition

3. DP uses native languages in the business domain

4. DP can be the smallest part of a business domain or can build complex combinations

Note: The concept and name for Domain Primitive comes from Dan Bergh Johnsson & Daniel Deogun’s book Secure by Design.

Three rules for using Domain Primitive

1. Make implicit concepts explicit

2. Make the implicit context explicit

Encapsulate multi-object behavior

The difference between Domain Primitive and DDD Value Object

In DDD, the concept of a Value Object already exists:

1. In Evans’ DDD Blue book, a Value Object is more of a non-entity Value Object

2. In Vernon’s IDDD red Book, the author paid more attention to Value Object Immutability, Equals method, Factory method, etc

Domain Primitive is an advanced version of Value Object that builds on the original VO and requires each DP to have a conceptual whole, not just a Value Object. Validity and behavior are added to Immutable VO. Of course, the same requirement is side-effect free.

The difference between Domain Primitive and Data Transfer Object (DTO)

Another data structure that you often encounter in everyday development is dtos, such as method input and output parameters. The differences between DP and DTO are as follows:

When should Domain Primitive be used

Common DP usage scenarios include:

Strings with format constraints such as Name, PhoneNumber, OrderNumber, ZipCode, Address, etc

2. Restricted Integer: OrderId (>0), Percentage (0-100%), Quantity (>=0), etc

3, enumerable int: such as Status (usually not Enum due to deserialization problems)

Double or BigDecimal: Commonly used Double or BigDecimal have business meanings such as Temperature, Money, Amount, ExchangeRate, Rating, etc

5, complex data structures: such as Map<String, List>, etc., try to wrap all operations of Map, only expose the necessary behavior

Field – The process of refactoring old applications

Using DP in a new application is relatively simple, but using DP in an old application is a step-by-step upgrade that can follow the following process. Use the first case in this article as an example.

Step 1 – Create Domain Primitive and collect all DP actions

In the previous article, we found that taking the area code of the PhoneNumber is a separate logic that fits into the PhoneNumber Class. Similarly, in a real project, code that was previously scattered in various services or utility classes can be pulled out and put into the DP to become its own behavior or properties. The principle here is that all extracted methods should be stateless, such as methods that were static. If the original method has a state change, you need to separate the part that changes state from the part that does not change state, and then integrate the stateless part into DP. Since DP itself cannot have state, any code that needs to change state is not DP’s domain.

(Refer to PhoneNumber’s code, which will not be repeated here)

Step 2 – Replace the data checksum stateless logic

In order to ensure the compatibility of the existing methods, the interface signature is not changed in the second step, but the original verification logic and the business logic related to the root DP are replaced by the code. Such as:

public User register(String name, String phone, String address)
        throws ValidationException {
    if (name == null || name.length() == 0) {
        throw new ValidationException("name");
    }
    if(phone == null || ! isValidPhoneNumber(phone)) { throw new ValidationException("phone");
    }
    
    String areaCode = null;
    String[] areas = new String[]{"0571"."021"."010"};
    for (int i = 0; i < phone.length(); i++) {
        String prefix = phone.substring(0, i);
        if (Arrays.asList(areas).contains(prefix)) {
            areaCode = prefix;
            break; } } SalesRep rep = salesRepRepo.findRep(areaCode); // Other code... }Copy the code

After replacing the code with DP:

public User register(String name, String phone, String address) throws ValidationException { Name _name = new Name(name); PhoneNumber _phone = new PhoneNumber(phone); Address _address = new Address(address); SalesRep rep = salesRepRepo.findRep(_phone.getAreaCode()); // Other code... }Copy the code

The new PhoneNumber(phone) code replaces the original verification code.

Replace the original stateless service logic with _phone.getareacode ().

Step 3 – Create a new interface

Create a new interface and push the DP code to the interface parameter layer:

public User register(Name name, PhoneNumber phone, Address address) {
    SalesRep rep = salesRepRepo.findRep(phone.getAreaCode());
}
Copy the code

Step 4 – Modify the external call

External callers need to modify the call link, for example:

service.register("YanHao"."0571-12345678".969 Wansan Xi Lu, Yuhang District, Hangzhou City, Zhejiang Province, China);
Copy the code

To:

service.register(new Name("YanHao"), new PhoneNumber("0571-12345678"), new Address(969 Wansan Xi Lu, Yuhang District, Hangzhou City, Zhejiang Province, China));
Copy the code

With these four steps, you can make your code simpler, more elegant, more robust, and more secure. What are you waiting for? Try it today!