preface

I apologize to the readers for the slow output of this article due to my busy work and pursuit of the quality of the article, but I can assure you that the content of this article has been repeatedly practiced and trudging. The first few articles in the DDD series can be read by clicking below the text

Lecture 1 of the DDD series

Lecture 2 of the DDD series

Lecture 3 of the DDD series

Lecture 4 of the DDD series

In the past year, our team have done a lot of the old system of refactoring and migration, there are a lot of code belongs to the laundry list, often can see is to develop in the API interface of foreign direct write business logic code, or in the amount of heap on a service interface, business logic actual can’t convergence, interface reusability is bad. Therefore, this lecture mainly wants to systematically explain how to transform the original running account code into logical and responsible modules through the reconstruction of DDD.

Case description

Here is a simple common case: single link. Suppose we are making a checkout interface that needs to perform various checks, query the product information, call the inventory service to deduct the inventory, and then generate the order:

A typical code would look like this:

@RestController @RequestMapping("/") public class CheckoutController { @Resource private ItemService itemService; @Resource private InventoryService inventoryService; @Resource private OrderRepository orderRepository; @PostMapping("checkout") public Result<OrderDO> checkout(Long itemId, Integer quantity) {/ / 1) Session management Long userId. = SessionUtils getLoggedInUserId (); if (userId <= 0) { return Result.fail("Not Logged In"); } / / check if 2) parameters (itemId < = 0 | | quantity < = 0 | | quantity > = 1000) {return Result. The fail (" gave the Args "); } // 3) ItemDO item = ItemService.getitem (itemId); if (item == null) { return Result.fail("Item Not Found"); } / / 4) call an external service Boolean withholdSuccess = inventoryService, withhold (itemId and quantity); if (! withholdSuccess) { return Result.fail("Inventory not enough"); } // 5) field count Long cost = item.getPriceincents () * quantity; OrderDO order = new OrderDO(); order.setItemId(itemId); order.setBuyerId(userId); order.setSellerId(item.getSellerId()); order.setCount(quantity); order.setTotalCost(cost); . / / 7) data persistence orderRepository createOrder (order); // 8) Return result. success(order); }}Copy the code

Why is this typical ledger code problematic in practice? The essential problem is that it violates the Single Responsbility Principle (SRP). This code in the mixed business calculation, calibration logic, infrastructure, and communication protocol, etc., in the future no matter what part of the logic of change will directly affect the code, when the posterity continuously for a long time in the above stacking a new logic, will cause the code complexity increases, logic branch is becoming more and more and eventually cause bugs or nobody dare to reconstruct the historical burden.

So we need to use DDD layering idea to reconstruct the above code, through different code layering and specification, split out logical, responsibility clear layering and module, but also facilitate some general ability precipitation.

The main steps are as follows:

  1. A separate Interface layer is responsible for handling network protocol-related logic
  2. Use Cases are identified from real business scenarios, and then specific Use Cases are followed by dedicated Command commands, Query queries, and Event Event objects
  3. Separate Application layer, responsible for orchestration of business processes, response to Command, Query, and Event. Each application layer method should represent a node in the overall business process
  4. Handles cross-cutting concerns across layers, such as authentication, exception handling, validation, caching, logging, and so on

Each point is explained in detail below.

Interface Interface layer

With the popularity of REST and MVC architectures, it is common to see developers writing business logic directly in controllers, as in the typical case above, but MVC Controllers are not the only disaster area. There are several common code forms that may contain the same problem:

  • HTTP framework: Such as Spring MVC framework, Spring Cloud, etc
  • RPC framework: such as Dubbo, HSF, gRPC, etc
  • Message queue “consumers” of MQ: such as JMS’s onMessage, RocketMQ’s MessageListener, etc
  • Socket communication: Receive of Socket communication, onMessage of WebSocket, etc
  • File system: WatcherService etc
  • Distributed task scheduling: SchedulerX, etc

The common point of these methods is that they all have their own network protocol. If our business code and network protocol are mixed together, the code will be directly bound to the network protocol and cannot be reused. Therefore, in the hierarchical architecture of DDD, we will separate the Interface layer as the gateway to all external, and decouple the network protocol and business logic.

Composition of the interface layer

The interface layer mainly consists of the following functions:

  1. Translation of network protocols: Usually this has been encapsulated by various frameworks, and we need to build classes that are either annotated beans or beans that inherit from an interface.
  2. Unified authentication: For example, in some scenarios that require AppKey+Secret, authentication is performed for a tenant, including the verification of some encrypted strings
  3. Session management: Generally in the user-facing interface or login mode, the Session or RPC context can be used to retrieve the current calling user for passing to the downstream service.
  4. Traffic limiting: Traffic limiting is implemented on interfaces to prevent heavy traffic from being sent to downstream services
  5. Front-loading: For read-only scenarios where changes are infrequent, front-loading results to the interface layer is possible
  6. Exception handling: It is generally necessary to avoid directly exposing exceptions to callers at the interface layer. Therefore, uniform exception capture is required at the interface layer and data format can be understood by the callers
  7. Log: Logs are generated at the interface layer to perform statistics and debug. Most microservices frameworks probably include these capabilities directly.

Of course, if you have a separate gateway facility/application, you can separate the authentication, Session, traffic limiting, logging and other logic, but currently API gateways can only solve some of the functions, even in the case of API gateways, a separate interface layer in the application is still necessary. At the interface layer, authentication, Session, traffic limiting, caching, and logging are straightforward, except for exception handling.

Return value and Exception handling specification, Result vs Exception

Note: This section is mainly for REST and RPC interfaces, other protocols need to generate return values according to the protocol specification.

In some of the code I’ve seen, the interface returns a variety of values, some directly returning DTO or even DO, and others returning Result. The core value of the interface layer is external, so simply returning DTO or DO will inevitably expose the exception and error stack to the user, including the cost of the error stack being serialized and deserialized. So, here is a specification:

Specification: HTTP and RPC interfaces at the Interface layer that return Result and catch all exceptions

Specification: All interfaces in the Application layer return dTOS and are not responsible for handling exceptions

I’ll talk about the specification of the Application layer later, but I’ll show you the logic of the Interface layer first.

Here’s an example:

@PostMapping("checkout") public Result<OrderDTO> checkout(Long itemId, Integer quantity) { try { CheckoutCommand cmd = new CheckoutCommand(); OrderDTO orderDTO = checkoutService.checkout(cmd); return Result.success(orderDTO); } the catch (ConstraintViolationException cve) {/ / capture some special exceptions, such as abnormal return the Validation Result. The fail (cve. GetMessage ()); } catch (Exception e) {return result. fail(LLDB etMessage()); }}Copy the code

Of course, writing exception handling logic for each interface can be annoying, so you can use AOP to make annotations

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ResultHandler { } @Aspect @Component public class ResultAspect { @Around("@annotation(ResultHandler)") public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { Object proceed = null; try { proceed = joinPoint.proceed(); } catch (ConstraintViolationException cve) { return Result.fail(cve.getMessage()); } catch (Exception e) { return Result.fail(e.getMessage()); } return proceed; }}Copy the code

Then the final code is simplified to:

@PostMapping("checkout")
@ResultHandler
public Result<OrderDTO> checkout(Long itemId, Integer quantity) {
    CheckoutCommand cmd = new CheckoutCommand();
    OrderDTO orderDTO = checkoutService.checkout(cmd);
    return Result.success(orderDTO);
}
Copy the code

The number of interfaces at the interface layer and the isolation of services

In the traditional REST and RPC interface specifications, generally a domain interface, whether it is REST Resource GET/POST/DELETE resources, or RPC methods, is relatively fixed and unified, and will seek to unify the domain methods in a domain service or Controller.

However, I find that in the actual business process, especially when there are many upstream businesses supported, the deliberate pursuit of interface unification usually leads to the parameter expansion in the method, or the method expansion. For example: suppose there is a pet card and a parent-child card business share the same card opening service, but the pet needs to pass in the pet type, and the parent-child needs to pass in the baby age.

Public interface CardService {// 1) Parameter expansion Result openCard(int petType, int babyAge); Result openCardV2(Map<String, Object> params); // Result openCardV2(Map<String, Object> params); // 3) Result openPetCard(int petType); Result openBabyCard(int babyAge); }Copy the code

It can be seen that no matter how the operation is done, CardService may become more and more difficult to maintain in the future with more and more methods. The change of one business may lead to the change of the whole service /Controller and finally become unmaintainable. I once participated in a service that provided dozens of methods and tens of thousands of lines of code. As you can imagine, both the cost of understanding the interface and the cost of maintaining the code were extremely high. So, here’s another specification:

Specification: An Interface layer class should be “small and beautiful” and should be geared toward “a single business” or “a class of business with the same requirements,” avoiding the need to use the same class to meet the requirements of different types of business.

Based on the above specification, it can be found that although pet card and parent-child card seem to have similar needs, they are not “the same needs”. It can be predicted that at some point in the future, the needs and interfaces of these two businesses will be further and further apart, so it is necessary to separate these two interface classes:

public interface PetCardService {
    Result openPetCard(int petType);
}

public interface BabyCardService {
    Result openBabyCard(int babyAge);
}
Copy the code

The benefit of this is that it complies with the Single Responsibility Principle, which states that an interface class changes only when a business (or class) changes. One suggestion is to split an existing interface class when it is too large. The splitting principle is the same as that of SRP.

One might ask, if you do this, will you end up with a lot of interface classes that duplicate code logic? The answer is no, because in the DDD layered architecture, the core function of the interface class is only the protocol layer. The protocol of each business class can be different, and the real business logic will be deposited into the application layer. That is, the relationship between Interface and Application is many-to-many:

Since business requirements change rapidly, the interface layer should also change rapidly. The interaction between businesses can be avoided through the independent interface layer, but we want the logic of the Application layer to be relatively stable. So let’s look at some of the Application layer specifications.

The Application layer

Components of the Application layer

Application layer core classes:

  • ApplicationService: The core class responsible for orchestration of business processes, but not for any business logic itself
  • DTO Assembler: Responsible for converting internal domain models into externally available DTO
  • Command, Query, Event objects: as input arguments to ApplicationService
  • Dtos returned: as an exit argument to ApplicationService

The core object of the Application layer is ApplicationService, whose core function is to undertake “business processes”. But before we get to the specification of ApplicationService, we must first focus on a few special types of objects, namely Command, Query, and Event.

Command, Query, Event objects

In essence, these objects are all Value objects, but they are quite different from each other in semantics:

  • Command: An instruction that the caller explicitly wants the system to operate with the expectation that it will affect a system, namely, a write operation. In general, instructions need to have an explicit return value (such as synchronous operation result, or asynchronous instruction already received).
  • Query: Something that the caller explicitly wants to Query, including Query parameters, filtering, paging, etc., and is expected to have no impact on a system’s data.
  • Event: Refers to an existing Event that has happened and requires the system to change or respond to it. Usually, Event processing involves writing operations. The event handler does not return a value. It is important to note that the concept of an Event in the Application layer is similar to that in the Domain layer, but not necessarily the same. The Event here is more of an external notification mechanism.

To summarize briefly:

Command Query Event
semantic Actions that you want to trigger Query of various conditions Things that have already happened
Read/write write read-only Usually write
The return value Dtos or Boolean Dtos or Collection Void

Why use CQE objects?

It is common to see multiple parameters on an interface in a lot of code, as in the example above:

Result<OrderDO> checkout(Long itemId, Integer quantity);
Copy the code

If you need to add parameters to the interface, you need to add a method for forward compatibility:

Result<OrderDO> checkout(Long itemId, Integer quantity);
Result<OrderDO> checkout(Long itemId, Integer quantity, Integer channel);
Copy the code

Or the common query method, due to different conditions lead to multiple methods:

List<OrderDO> queryByItemId(Long itemId);
List<OrderDO> queryBySellerId(Long sellerId);
List<OrderDO> queryBySellerIdWithPage(Long sellerId, int currentPage, int pageSize);
Copy the code

As can be seen, the traditional interface writing method has several problems:

  1. Interface bloat: One query condition and one method
  2. Difficult to scale: each new parameter may require a caller upgrade
  3. Difficult to test: With multiple interfaces, responsibilities become complicated, business scenarios vary, and test cases are difficult to maintain

But the other most important problem is that this type of parameter list itself does not have any business “meaning”, just a bunch of parameters, can not clearly express the intention.

Specification of CQE:

So one strongly recommended specification in the Application layer interface is:

Specification: The interface input parameter of ApplicationService can only be a Command, Query, or Event object. The CQE object must represent the semantics of the current method. The only exception to this rule is for queries based on a single ID, where the creation of a Query object can be omitted

According to the above specification, the implementation case is:

public interface CheckoutService { OrderDTO checkout(@Valid CheckoutCommand cmd); List<OrderDTO> query(OrderQuery query); OrderDTO getOrder(Long orderId); Query} @data public class CheckoutCommand {private Long userId; private Long itemId; private Integer quantity; } @Data public class OrderQuery { private Long sellerId; private Long itemId; private int currentPage; private int pageSize; }Copy the code

The benefits of this specification are improved interface stability, reduced low-level duplication, and more semantic interface input parameters.

CQE vs DTO

As you can see from the above code, the ApplicationService takes a CQE object as an input and a DTO as an output. The code format is simple POJO objects.

  • CQE: The CQE object is the input to ApplicationService. It has an explicit intent, so the object must be guaranteed to be “correct”.
  • DTO: A DTO object is just a data container, intended only to interact with the outside world, so it contains no logic and is anaemic.

But perhaps the most important point: because CQE is intent, there could theoretically be an infinite number of CQE objects, each representing a different intent; However, as a model data container, DTO corresponds to the model one by one, so it is limited.

CQE in check

CQE is the input to the ApplicationService and must be guaranteed to be correct. Where does this check go? In the earliest code, there was this validation logic, written in the service:

if (itemId <= 0 || quantity <= 0 || quantity >= 1000) {
    return Result.fail("Invalid Args");
}
Copy the code

This kind of code is very common in everyday life, but the biggest problem is that a lot of non-business code is mixed in with business code, which clearly violates the single responsibility principle. But since the input was a simple int at the time, this logic could only occur in the service. Now that the input parameter has been changed to CQE, we can use Bean Validation from the Java standard JSR303 or JSR380 to prepopulate this Validation logic.

Specification: CQE objects should be validated first, avoiding validation of parameters in ApplicationService. This can be done with jSR303/380 and Spring Validation

The previous example can be rewritten as:

Public class CheckoutServiceImpl implements CheckoutService {OrderDTO checkout(@valid) CheckoutCommand CMD) {// @valid jSR-303/380 }} @data public class CheckoutCommand {@notnull (message = "user not logged in ") private Long userId; @notnul@positive (message = "valid itemId") private Long itemId; @notnul@min (value = 1, message = "at least 1 pieces ") @max (value = 1000, message =" at most 1000 pieces ") private Integer quantity; }Copy the code

The advantage of this approach is that it makes the ApplicationService cleaner and the error messages can be customized through the Bean Validation API.

Avoid CQE reuse

Because CQE has “intention” and “semantics”, we need to avoid the reuse of CQE objects, even if all parameters are the same, as long as their semantics are different, we should try to use different objects.

Specification: Avoid reuse of CQE objects for instructions with different semantics

❌ counterexample: A common scenario is “Create Create” and “Update Update”. Generally speaking, the only difference between these two types of objects is an ID. The creation does not have an ID, while the Update does. So it’s not uncommon to see students using the same object as an input parameter to both methods, the only difference being whether the ID is assigned. This is incorrect because the semantics of these two operations are completely different, and their validation conditions may be completely different, so you should not reuse the same object. The correct approach is to produce two objects:

public interface CheckoutService { OrderDTO checkout(@Valid CheckoutCommand cmd); OrderDTO updateOrder(@Valid UpdateOrderCommand cmd); } @data public class UpdateOrderCommand {@notnull (message = "user not logged in ") private Long userId; @notnull (message = "must have OrderID") private Long OrderID; @notnul@positive (message = "valid itemId") private Long itemId; @notnul@min (value = 1, message = "at least 1 pieces ") @max (value = 1000, message =" at most 1000 pieces ") private Integer quantity; }Copy the code

ApplicationService

ApplicationService is responsible for orchestration of business processes. It is the “glue layer” code that separates the original business ledger code from the validation logic, domain calculation, persistence, and other logic.

Consider a simple trading process:

In this case, we can see that there are five use cases in the field of transaction: placing an order, paying successfully, closing an order with failed payment, updating logistics information and closing an order. These five use cases can be replaced with five Command/Event objects, which correspond to five methods.

I have seen three types of organization for ApplicationService:

  1. An ApplicationService class is a complete business process in which each method is responsible for handling a Use Case. The advantage of this is that the whole business logic can be completely converged, and the business logic can be mastered from the interface class, which is suitable for relatively simple business processes. The downside is that complex business processes can lead to a class with too many methods and possibly too much code. Examples of this type are:
Public interface CheckoutService {// Order OrderDTO checkout(@valid CheckoutCommand CMD); OrderDTO payReceived(@valid PaymentReceivedEvent event); // Cancel OrderDTO payCanceled(@valid PaymentCanceledEvent event); OrderDTO packageSent(@valid PackageSentEvent Event); // OrderDTO delivered(@valid DeliveredEvent event); List<OrderDTO> query(OrderQuery query); // single query OrderDTO getOrder(Long orderId); }Copy the code
  1. For more complex business processes, you can reduce the amount of code in a class by adding a separate CommandHandler, EventHandler:
@Component public class CheckoutCommandHandler implements CommandHandler<CheckoutCommand, OrderDTO> { @Override public OrderDTO handle(CheckoutCommand cmd) { // } } public class CheckoutServiceImpl implements CheckoutService { @Resource private CheckoutCommandHandler checkoutCommandHandler; @Override public OrderDTO checkout(@Valid CheckoutCommand cmd) { return checkoutCommandHandler.handle(cmd); }}Copy the code
  1. In a more radical way, the command or event is directly thrown to the corresponding Handler using CommandBus and EventBus, which are common. The example code is as follows: After receiving an MQ message through the message queue, an Event is generated and routed to the corresponding Handler by EventBus:
// Application layer // Where the framework usually recognizes the PaymentReceivedEvent by interface // it can also recognize @Component public class by adding annotations PaymentReceivedHandler implements EventHandler<PaymentReceivedEvent> { @Override public void Process (PaymentReceivedEvent Event) {//}} // Listener Public Class OrderMessageListener implements MessageListenerOrderly {@Resource Private EventBus eventBus; @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) { PaymentReceivedEvent event = new PaymentReceivedEvent(); eventBus.dispatch(event); / / do not need to specify the consumer return ConsumeOrderlyStatus. SUCCESS; }}Copy the code

⚠️ not recommended: this approach can implement a completely static decoupling of the Interface layer and a specific ApplicationService or Handler, and dispatch dynamically at run time, preferably with frameworks such as the AxonFramework. Although it looks convenient, according to our own business practice and practice, when the number of CQE objects in the code increases and the handler becomes more and more complex, the dispatch at runtime lacks the association between static codes, which makes the code difficult to read, especially when you need to trace a complex call link. Because Dispatch is run time, it’s hard to figure out what object is being invoked. So we’ve tried that before, but it’s not recommended anymore.

Application Services encapsulate business processes and do not handle business logic

Although it has been repeated countless times before that ApplicationService is only responsible for business process concatenation, not business logic, how do you determine whether a piece of code is a business process or logic? To take an example from earlier, after the initial code refactoring:

  1. Don’t have if/else branch logic: the Cyclomatic Complexity of your code should be as equal to 1 as possible

Generally, branch logic represents some business decisions and should be encapsulated in DomainService or Entity. But that doesn’t mean that I could not have the if logic, for example, in this code: Boolean withholdSuccess = inventoryService, withhold (CMD) getItemId (), CMD. GetQuantity ()); if (! withholdSuccess) { throw new IllegalArgumentException(“Inventory not enough”); } although CC > 1, it only represents an interrupt condition, and the specific business logic processing is not affected. You can think of it as Precondition.

@Service
@Validated
public class CheckoutServiceImpl implements CheckoutService {

    private final OrderDtoAssembler orderDtoAssembler = OrderDtoAssembler.INSTANCE;
    @Resource
    private ItemService itemService;
    @Resource
    private InventoryService inventoryService;
    @Resource
    private OrderRepository orderRepository;

    @Override
    public OrderDTO checkout(@Valid CheckoutCommand cmd) {
        ItemDO item = itemService.getItem(cmd.getItemId());
        if (item == null) {
            throw new IllegalArgumentException("Item not found");
        }

        boolean withholdSuccess = inventoryService.withhold(cmd.getItemId(), cmd.getQuantity());
        if (!withholdSuccess) {
            throw new IllegalArgumentException("Inventory not enough");
        }

        Order order = new Order();
        order.setBuyerId(cmd.getUserId());
        order.setSellerId(item.getSellerId());
        order.setItemId(item.getItemId());
        order.setItemTitle(item.getTitle());
        order.setItemUnitPrice(item.getPriceInCents());
        order.setCount(cmd.getQuantity());

        Order savedOrder = orderRepository.save(order);

        return orderDtoAssembler.orderToDTO(savedOrder);
    }
}
Copy the code
  1. Don’t have any calculations:

In the earliest code there is this calculation:

Long cost = item.getPriceincents () * quantity; order.setTotalCost(cost);Copy the code

By encapsulating this calculation logic into an entity, you avoid doing calculations in ApplicationService

@Data public class Order { private Long itemUnitPrice; private Integer count; Public Long getTotalCost() {return itemUnitPrice * count; } } order.setItemUnitPrice(item.getPriceInCents()); order.setCount(cmd.getQuantity());Copy the code
  1. Some data can be converted to other objects:

DTO Assembler, for example, precipitates the logic converted between objects in a separate class, reducing the complexity of ApplicationService

OrderDTO dto = orderDtoAssembler.orderToDTO(savedOrder);
Copy the code

Common ApplicationService “Routines”

As you can see, ApplicationService code usually has a similar structure: AppService typically does not make any decisions (except preconditions), just feed all decisions to the DomainService or Entity and feed the external interactions to the Infrastructure interface, such as Repository or preservative layer.

The general “routine” is as follows:

  • Prepare data: This includes fetching the corresponding Entity, VO, and DTOS returned by the external service from the external service or persistent source.
  • Perform operations: This includes creating new objects, assigning values, and manipulating them by calling the domain object’s methods. Note that this is usually a pure memory operation, non-persistent.
  • Persistence: Persisting the results of an operation or performing asynchronous operations such as sending messages to external systems.

If there are changes to multiple external systems (including its own DB), this is usually in the “distributed transaction” scenario, whether using distributed TX, TCC, or Saga mode, depending on the design of the specific scenario, which is skipped here.

DTO Assembler

An often overlooked question is should ApplicationService return Entity or DTO? Here is a specification proposed in the DDD hierarchy:

ApplicationService should always return a DTO instead of an Entity

Why is that?

  1. Construct domain boundaries: ApplicationService takes CQE objects as its input and DTos as its output, which are basically simple POJos to ensure that the inside and outside of the Application layer do not interfere with each other.
  2. Reduce rule dependencies: Entities usually contain business rules, and if ApplicationService returns an Entity, it causes the caller to rely on the business rules directly. If the internal rule changes may directly affect the external.
  3. Reduce costs through DTO combination: Entity is limited, DTO can be a free combination of multiple Entity and VO, and can be encapsulated as a complex DTO at one time, or select some parameters extracted and encapsulated as DTO to reduce external costs.

Because we operate on an Entity but output an object as a DTO, we need an object of a proprietary type called a DTO Assembler. The sole responsibility of the DTO Assembler is to convert one or more Entity/VO to a DTO. Note: DTO Assembler generally does not recommend inverse operations, that is, not from DTO Entity, because there is usually no guarantee of Entity accuracy when a DTO is converted to Entity.

In general, Entity to DTO has a cost, both in terms of code volume and runtime operations. Handwritten conversion code is error-prone, and using Reflection to save code will cause a significant performance loss. So I’m going to go out of my way to recommend MapStruct. MapStruct is generated through static compile-time code generation. The corresponding code can be generated by writing interfaces and configuring annotations, and since the generated code is directly assigned, the performance loss is negligible.

With MapStruct, the code can be simplified to:

import org.mapstruct.Mapper; @Mapper public interface OrderDtoAssembler { OrderDtoAssembler INSTANCE = Mappers.getMapper(OrderDtoAssembler.class); OrderDTO orderToDTO(Order order); } public class CheckoutServiceImpl implements CheckoutService { private final OrderDtoAssembler orderDtoAssembler = OrderDtoAssembler.INSTANCE; @Override public OrderDTO checkout(@Valid CheckoutCommand cmd) { // ... Order order = new Order(); / /... Order savedOrder = orderRepository.save(order); return orderDtoAssembler.orderToDTO(savedOrder); }}Copy the code

Combined with the previous Data Mapper, the relationship between DTO, Entity and DataObject is shown as follows:

Result vs Exception

Finally, the Interface layer should return Result and the Application layer should return DTO. Here we repeat the specification:

The Application layer only returns Dtos and can throw exceptions directly without unified processing. All called services can also throw exceptions directly, without intentionally catching exceptions unless special handling is required

The advantage of exception is that it can clearly know the source of the error, stack, etc. Uniformly capturing exceptions in the Interface layer is to avoid the leakage of exception stack information outside the API, but in the Application layer, the exception mechanism is still the method with the largest amount of information and the clearest code structure. Avoid some common and complicated Result. IsSuccess judgment of Result. Therefore, at the Application layer, Domain layer, and Infrastructure layer, throwing exceptions directly when encountering errors is the most reasonable approach.

Let’s talk briefly about the anti-corruption Layer

This article only briefly describes the principles and functions of ACLs, the specific implementation specifications may wait for another article.

In ApplicationService, external services are often relied upon, creating dependencies on external systems at the code level. Such as above:

ItemDO item = itemService.getItem(cmd.getItemId());
boolean withholdSuccess = inventoryService.withhold(cmd.getItemId(), cmd.getQuantity());
Copy the code

You’ll see that our ApplicationService strongly relies on ItemService, InventoryService, and ItemDO objects. If the methods of any of the services change, or the ItemDO field changes, it may affect the ApplicationService code. That is, our own code will change due to strong dependence on external system changes, which should be avoided in complex systems. So how do you isolate external systems? Add an ACL anti-corrosion layer.

The simple principle of ACL anticorrosion layer is as follows:

  • For dependent external objects, we extract the required fields to generate an internally required VO or DTO class
  • Build a new Facade that encapsulates the invocation link to transform the external class into an inner class
  • For external system calls, the same Facade method wraps external call links

No anti-corrosion layer:

There is an anti-corrosion layer:

For a simple implementation, assume that all external dependencies are named ExternalXXXService:

@data Public Class ItemDTO {private Long itemId; private Long sellerId; private String title; private Long priceInCents; } public interface ItemFacade {ItemDTO getItem(Long itemId); } @service public class ItemFacadeImpl implements ItemFacade {@resource Private ExternalItemService externalItemService; @Override public ItemDTO getItem(Long itemId) { ItemDO itemDO = externalItemService.getItem(itemId); if (itemDO ! = null) { ItemDTO dto = new ItemDTO(); dto.setItemId(itemDO.getItemId()); dto.setTitle(itemDO.getTitle()); dto.setPriceInCents(itemDO.getPriceInCents()); dto.setSellerId(itemDO.getSellerId()); return dto; } return null; Int count count count count count count count count count count count count count count count count; } @Service public class InventoryFacadeImpl implements InventoryFacade { @Resource private ExternalInventoryService externalInventoryService; @Override public boolean withhold(Long itemId, Integer quantity) { return externalInventoryService.withhold(itemId, quantity); }}Copy the code

After the ACL modification, our ApplicationService code is changed to:

@Service
public class CheckoutServiceImpl implements CheckoutService {

    @Resource
    private ItemFacade itemFacade;
    @Resource
    private InventoryFacade inventoryFacade;
    
    @Override
    public OrderDTO checkout(@Valid CheckoutCommand cmd) {
        ItemDTO item = itemFacade.getItem(cmd.getItemId());
        if (item == null) {
            throw new IllegalArgumentException("Item not found");
        }

        boolean withholdSuccess = inventoryFacade.withhold(cmd.getItemId(), cmd.getQuantity());
        if (!withholdSuccess) {
            throw new IllegalArgumentException("Inventory not enough");
        }

    		// ...
    }
}
Copy the code

Obviously, the benefit of this is that the ApplicationService code no longer relies directly on external classes and methods at all, but on our own internally defined value classes and interfaces. If there are any future changes to the external service, it is the Facade class and data transformation logic that need to be modified, not the Logic of ApplicationService.

Repository can be thought of as a special ACL that hides the details of data operations. Even if the underlying database structure changes, the database type changes, or other persistence methods are added, The Repository interface remains stable and ApplicationService remains the same.

In some theoretical frameworks an ACL Facade is also called a Gateway, meaning the same thing.

Orchestration vs Choreography

At the end of this article, I want to talk about design specifications for complex business processes. In complex business processes, we typically face two patterns: Orchestration and Choreography. Very helpless, these two English words of Baidu translation/Google translation, are “choreography”, but in fact these two modes are completely different design mode. Orchestration (such as Service Orchestration for SOA/ microservices) is a familiar usage, while Choreography has only recently become popular with the emergence of the event-driven architecture EDA. There may be other translations on the Internet, such as compilation, choreography, collaboration, etc., but they do not really express the meaning of the English words, so in order to avoid misunderstanding, I will try to use the original English words in the following. If anyone has a better translation method, please contact me.

Model introduction

Orchestration: Often a symphony Orchestra appears in the mind (note the similarity), as shown below. At the heart of a symphony orchestra is a Conductor. In a symphony, all musicians must obey the Conductor. So in Orchestration, all processes are triggered by a node or service. Our common business process code, which involves invoking external services, is called Orchestration and is triggered collectively by our services.

Choreography: The scene that usually comes to mind is a Choreography (from the Greek dance, Choros), as shown below. Each of the different dancers was doing their own thing, but there was no centralized conductor. By cooperating with each other and doing their own thing, the whole dance can present a complete and harmonious picture. So in the Choreography pattern, each service is an individual and may respond to some external events, but the whole system is a whole.

case

If they were to be used with Orchestration, the business logic would be to deduct funds from a pre-stored account when placing an order and to generate a logistic order to ship the order.

If this case were Choreography, the business logic would be: order, wait for a successful payment event, and then ship, something like this:

Pattern differentiation and choice

While it may seem like both models serve the same business purpose, in practice there are huge differences:

In terms of code dependencies:

  • Orchestration: Involves Orchestration of one service to another service that is strongly dependent on the service provider.
  • Choreography: Each service just does its own thing and triggers other services through events. There is no direct dependency between services to invoke. However, it is important to note that the downstream still depends on the upstream code (such as the event class), so you can assume that the downstream is dependent on the upstream.

In terms of code flexibility:

  • Orchestration: Because the dependencies between services are written to death, adding new business processes inevitably requires code changes.
  • Choreography: Since there is no direct call relationship between services, services can be added or replaced without changing upstream code.

From the call link:

  • Orchestration: Active Orchestration of calls from one service to another, thus command-driven instructions.
  • Choreography: Each service is passively triggered by an external Event, so it is event-driven.

From the perspective of business responsibilities:

  • Orchestration: Active callers (e.g., ordering services). Regardless of the downstream dependencies, the active caller is responsible for the overall business process and results.
  • Choreography: There is no active caller, each service only cares about its own triggering conditions and results, and no one service is responsible for the entire business link

To sum up a comparison:

Orchestration Choreography
Driving force Instruction Driven Command-driven Event-driven event-driven
Call depends on Upstream is strongly dependent on downstream No direct call dependencies but code dependencies can be considered downstream dependencies upstream
flexibility poor higher
Business duties Upstream is responsible for the business No overall responsibility person

It’s also important to be clear that the distinction between “command-driven” and “event-driven” is not “synchronous” and “asynchronous.” Directives can be invoked synchronously or fired by asynchronous messages (but asynchronous directives are not events); Conversely, an event can be an asynchronous message, but it can also be an in-process synchronous call. So the essence of the difference between command-driven and event-driven is not how it is called, but whether something has “already” happened.

So in your daily business, when you encounter a requirement, how do you choose between Orchestration or Choreography?

Here are two judgment methods:

  1. Specify the direction of dependencies:

The dependencies in the code are fairly clear: if you are downstream and the upstream doesn’t perceive you, you have to go event-driven; If the upstream must be aware of you, it can be driven by command. Conversely, if you are upstream and have a strong dependency on downstream, you are command driven; If it doesn’t matter who is downstream, you can go event-driven.

  1. Identify the “people in charge” of your business:

The second approach is to identify the “principals” in the business scenario. For example, if the business needs to notify the seller, the single responsibility of the ordering system should not be responsible for the notification of the message, but the order management system needs to actively trigger the message based on the advancement of the order status, so it is responsible for this function. In a complex business process, it is common to have both patterns, but it is also easy to design errors. If the dependencies are strange, or the call link/handler in the code is not clear, try switching the schema. It might be better.

Which model is better?

Obviously, there is no best model, only the best model for your business scenario.

❌ counterexamples: Event-driven Architecture (EDA) and reaction-Programming (LIKE RxJava), which have been popular in recent years, are innovative but partly “When you have a hammer, All problems are nails “is a classic case. They work wonders for event-based, stream-processing problems, but if you take these frameworks and put them into a command-driven business, the code will feel extremely “out of step” and the cognitive costs will increase. Therefore, in daily selection, parts of the process Orchestration and Choreography should be sorted out according to the business scenario, and then the corresponding framework should be selected.

Relationship to DDD hierarchy

Finally, with so much O vs C, what does DDD have to do with it? Is simple:

  • O&C is the focus of the Interface layer, Orchestration = external API, and Choreography = messages or events. After you decide O or C, you need to take on these “drivers” at the interface layer.
  • No matter how O&C is designed, the Application layer is “unaware” because ApplicationService inherently handles commands, Queries, and Events. The Interface layer decides what to do with these objects.

Therefore, although Orchestration and Choreography are two completely different business design patterns, the code that eventually falls to the Application layer should be the same, which is why the Application layer is a relatively stable existence of “use cases” rather than “interfaces”.

conclusion

As long as you are doing business, you will need to write business process and service choreography, but that doesn’t necessarily mean the code is of poor quality. Through the reasonable separation of Interface layer and Application layer in DDD’s hierarchical architecture, the code can become elegant and flexible, and can respond to business faster but at the same time can better precipitation. This article mainly introduces some code design specifications to help you master certain skills.

Interface layer:

  • Responsibilities: Mainly responsible for network protocol transformation, Session management, etc
  • Number of interfaces: Avoid the so-called unified API, and there is no need to artificially limit the number of interface classes. Only one set of interfaces is required for each/each type of service. Interface parameters should meet service requirements, and large and complete input parameters should be avoided
  • Interface outbound parameter: Result is returned
  • Exception handling: All exceptions should be caught to avoid leakage of exception information. AOP can be unified to avoid a lot of duplicate code in the code.

Application layer:

  • Input parameters: Instantiate Command, Query, and Event objects as input parameters to ApplicationService, with the exception of single-ID queries.
  • Semantics of CQE: CQE objects have semantics that differ from one use case to another, so avoid reuse even if the parameters are the same.
  • Entry Validation: Base Validation is handled through the Bean Validation API. Spring Validation comes with AOP for Validation, or you can write AOP yourself.
  • Output parameter: returns DTO, not Entity or DO.
  • DTO conversion: Use DTO Assembler to take care of Entity/VO conversion to DTO.
  • Exception handling: Exceptions are not captured uniformly and can be thrown at will.

Part of Infra layer:

  • The ACL anticorrosion layer transforms external dependencies into internal code, isolating external influences

Business process Design patterns:

  • There is no best model, depending on the business scenario, dependencies, and whether there is a business “owner.” Avoid reaching for nails with a hammer.

Looking forward to forecast

  • CQRS is a design mode of Application layer. It is a design concept based on the separation of Command and Query, from the simplest object separation to the most complex event-sourcing. This topic has many points to dig into and can be used frequently, especially with complex aggregations. I will talk about it separately later. The title is tentatively “The 7 Levels of CQRS”.
  • In today’s complex microservices development environment, it is inevitable to rely on services developed by external teams, but the cost of strong coupling (whether it is changes, code dependencies, or even indirect dependencies of Maven Jar packages) is a long-term issue for complex systems. The ACL anticorrosion layer is an isolation concept that removes the external coupling and makes the internal code more pure. There are many different types of ACL preservative layers. Repository is a special type of ACL for persistent face data. K8s-sidecar-istio is a network layer ACL, but there are more efficient and common methods in Java/Spring than ISTIO, which will be discussed later.
  • When you start using DDD, you will find that many code patterns are very similar. For example, the master sub-order pattern is the aggregate pattern, the CPV pattern of the category system can be used for some activities, the ECS pattern can be used for interactive business, and so on. In the following part, I will try to summarize some general domain design patterns, their design ideas, types of problems that can be solved, and methods of practice implementation.

Welcome to contact, continue to seek resume

Any questions about DDD are welcome and I will try my best to answer them. The code examples in this article will be posted to Github later for your reference. My email address: [email protected], or you can add my nail number: Luangm (Yin Hao)

At the same time, our team is constantly recruiting. My team is responsible for the department’s industry and shopping guide business. Including Tmall and taobao’s four big industry (apparel, consumer, electrical, decoration) and taobao several large horizontal business (enterprise service, global purchase, have a good product, etc.) of daily business needs and innovative business (3 d/AR, 360 panoramic video guide, collocation, custom, size, guide the SPU, etc.), the front desk (iFashion, global purchase, have a good product, etc.) , and some complex financial, transaction and performance links (IP matchmaking, financial services, transaction customization, distribution, CPS commission, service supply chain docking, etc.), and the total DAU (average daily users) is about 3000W. Our team has connected with a large number of business forms, from the front desk purchase guide to the background performance, with extremely rich application scenarios. In the new fiscal year, we hope to go deep into the industry, explore new business models and performance links, cover some business models that cannot be covered by the traditional B2C model, and help merchants grow in the new track. All interested students are welcome to join us.

The author | YanHao

Edit | orange

New retail product | alibaba tao technology