Martin Fowler, in his book Enterprise Application Architecture Patterns, writes:

I found this(business logic) a curious term because there are few things that are less logical than business logic.

Business logic is very illogical logic.

Indeed, there are times when the business logic of software cannot be deduced, or even imagined. The result is that an already complex business becomes even more complex and difficult to understand. In the specific coding implementation, in addition to dealing with business complexity, technical complexity can not be ignored, for example, we need to pay attention to the layering of technology, to follow the basic principles of software development, for example, to consider performance and security and so on.

In many projects, technical complexity and business complexity are intertwined, and this aggravation is the reason why many software projects fail to evolve further. With proper design, however, technology and business can be decoupled or at least decoupled. In different software modeling methods, Domain Driven Design (DDD) tries to solve the complexity of software problems through its own principles and routines. It focuses the eyes of developers on the business itself first, making the technical architecture and code implementation become “by-products” in the process of software modeling.

DDD overview

DDD is divided into strategic design and tactical design. In strategic design, we focus on the division of subdomains and Bounded contexts (BC), as well as the upstream and downstream relationship between each Bounded Context. The original logic behind the current hot topic “DDD in microservices” is that the bounded context in DDD can be used to guide service partitioning in microservices. Bounded context, in fact, still is an expression of software modularization, and we have been pursuing the principles of modular drivers are the same, namely through certain means to make the software system in the human brain to show more organized, people as “objective” can more easily understand and control software system.

If strategic design is more about software architecture, tactical design is more about coding. DDD tactics are designed to make the business separate and stand out from technology so that the code directly expresses the business itself, including concepts such as aggregation roots, application services, repositories, factories, and so on. Although DDD is not necessarily implemented through object Orientation (OO), it is often practiced in the OO programming paradigm, and there is even a saying in the industry that “DDD is OO advanced”, meaning that the basic principles of object orientation (such as SOLID) still hold true in DDD. This article focuses on tactical DDD design.

This article takes a simple e-commerce order system as an example, the source code can be obtained in the following ways:

git clone https://github.com/e-commerce-sample/order-backend

git checkout a443dace

There are three common ways to implement a business

Before getting into DDD, let’s take a look at some common ways to implement business code. In the example project, there is a business requirement to “modify the number of products in Order” as follows:

You can modify the number of products in the Order, but only if the Order is not paid. The totalPrice of the Order should be updated after the number of products is changed.

1. Implementation based on “Service + anemia model”

This approach is currently adopted by many software projects, and the main characteristics are: There is an anemic “domain object” where the business logic is implemented through a Service class, updated through setter methods, and finally saved to the database through DAO(probably using an ORM framework such as Hibernate in most cases). Implement an OrderService class as follows:

@Transactional
public void changeProductCount(String id, ChangeProductCountCommand command) {
    Order order = DAO.findById(id);
    if (order.getStatus() == PAID) {
        throw new OrderCannotBeModifiedException(id);
    }
    OrderItem orderItem = order.getOrderItem(command.getProductId());
    orderItem.setCount(command.getCount());
    order.setTotalPrice(calculateTotalPrice(order));
    DAO.saveOrUpdate(order);
}
Copy the code

This approach is still a process-oriented programming paradigm that violates the most basic OO principles. Another problem is that the division of responsibilities is unclear, causing the business logic that should be aggregated in the Order to leak elsewhere (OrderService), resulting in the Order becoming an Anemic Model that acts as a data container rather than a true domain Model. As the project continues to evolve, the business logic is scattered across different Service classes, with the end result that the code becomes increasingly difficult to understand and becomes less scalable.

2. Implementation based on transaction script

In the previous implementation, we saw that the only purpose of the domain object (Order) was to allow tools like ORM to persist once and for all, and that the domain object didn’t even need to exist without ORM. The code implementation then degenerates to Transaction scripts, which store the results calculated in the Service class directly to the database (or sometimes implement the business logic directly in SQL without the Service class at all):

@Transactional
public void changeProductCount(String id, ChangeProductCountCommand command) {
    OrderStatus orderStatus = DAO.getOrderStatus(id);
    if (orderStatus == PAID) {
        throw new OrderCannotBeModifiedException(id);
    }
    DAO.updateProductCount(id, command.getProductId(), command.getCount());
    DAO.updateTotalPrice(id);
}
Copy the code

As you can see, there are many more methods in the DAO, and the DAO is no longer just an encapsulation of persistence, but also contains business logic. In addition, the implementation of the dao.UpdateTotalPrice (ID) method calls SQL directly to update the Order total price. Similar to the “Service+ anaemic model” approach, transaction scripts have the problem of decentralized business logic.

In fact, transaction scripting is not a complete anti-pattern and can be adopted if the system is simple enough. But on the one hand, simplicity is not easy to grasp. On the other hand, software systems often add more functions as they evolve, making simple code increasingly complex. Therefore, transaction scripts are not used much in practical applications.

3. Implementation based on domain objects

In this approach, the core business logic is clustered in a behavior-rich domain object (Order), which implements the Order class as follows:

public void changeProductCount(ProductId productId, int count) {
    if (this.status == PAID) {
        throw new OrderCannotBeModifiedException(this.id);
    }
    OrderItem orderItem = retrieveItem(productId);
    orderItem.updateCount(count);
}
Copy the code

Then, in Controller or Service, call order.changeProductCount () :

@PostMapping("/order/{id}/products")
public void changeProductCount(@PathVariable(name = "id") String id, @RequestBody @Valid ChangeProductCountCommand command) {
    Order order = DAO.byId(orderId(id));
    order.changeProductCount(ProductId.productId(command.getProductId()), command.getCount());
    order.updateTotalPrice();
    DAO.saveOrUpdate(order);
}
Copy the code

As you can see, all the businesses (” Check Order status, “” modify Product quantity,” and “update Order total price”) are contained in the Order object, which is what an Order should do. (There is, however, one area in the sample code that clearly violates the rule of cohesion, which you, as a suspense reader, should try to look for.)

In fact, this approach is very similar to the DDD tactical pattern described in this article, but DDD abstracts more concepts and principles.

Subcontracting based on business

In the previous article in this series, Spring Boot Project Templates, I actually covered business based subcontracting, in combination with the DDD scenario, and I’ll briefly discuss it here. Subcontracting based on business refers to the modular division of business functions implemented by software, rather than from a technical point of view (such as service and Infrastruture packages first). In the strategic design of DDD, we focus on overlooking the entire software system from a macro perspective, and then divide the system into subdomains and bounded contexts according to certain principles. In tactical practice, we also plan the overall code structure in a similar way by aiming at the basics of cohesion and separation of responsibilities. At this point, the first thing that comes into view is the subcontracting of software.

In DDD, the aggregation root (described below) is the carrier of the main business logic and is a good example of the “cohesion” principle, so it is common practice to partition the top-level packages based on the aggregation root. In the example e-commerce project, there are two aggregate root objects Order and Product. Create the Order package and Product package respectively, and then divide the molecular package under the top-level package according to the complexity of the code structure. For example, for the Product package:

└ ─ ─ the product ├ ─ ─ CreateProductCommand. Java ├ ─ ─ the product. The Java ├ ─ ─ ProductApplicationService. Java ├ ─ ─ ProductController. Java ├ ─ ─ ProductId. Java ├ ─ ─ ProductNotFoundException. Java ├ ─ ─ ProductRepository. Java └ ─ ─ representation ├ ─ ─ ProductRepresentationService. Java └ ─ ─ ProductSummaryRepresentation. JavaCopy the code

As you can see, most classes, such as ProductRepository and ProductController, are placed directly under the Product package rather than being subcontracted separately; But it did show class ProductSummaryRepresentation subcontract alone. The principle here is that in cases where all classes are already clustered under the Product package, there is no need to split subpackages again if the code structure is simple enough, as is the case with ProductRepository and ProductController; If multiple class cohesion, needs to be done again you need to subcontract separately, such as through the REST API interface to return the Product data, the code involves two objects ProductRepresentationService and ProductSummaryRepresentation, These two objects are closely related, so we place them under the representation subpackage. For more complex orders, subcontracting is as follows:

├ ─ ─ the order │ ├ ─ ─ OrderApplicationService. Java │ ├ ─ ─ OrderController. Java │ ├ ─ ─ OrderPaymentProxy. Java │ ├ ─ ─ OrderPaymentService. Java │ ├ ─ ─ OrderRepository. Java │ ├ ─ ─ the command │ │ ├ ─ ─ ChangeAddressDetailCommand. Java │ │ ├ ─ ─ ├─ ├─ Java │ ├─ ├.java │ ├─ ├.java │ ├─ ├.java │ ├─ UpdateProductCountCommand. Java │ ├ ─ ─ exception │ │ ├ ─ ─ OrderCannotBeModifiedException. Java │ │ ├ ─ ─ OrderNotFoundException. Java │ │ ├ ─ ─ PaidPriceNotSameWithOrderPriceException. Java │ │ └ ─ ─ ProductNotInOrderException. Java │ ├─ Model │ │ ├─ Order. Java │ │ ├─ OrderId. Java │ │ ├─ OrderIdGenerator OrderItem. Java │ │ └ ─ ─ OrderStatus. Java │ └ ─ ─ representation │ ├ ─ ─ OrderItemRepresentation. Java │ ├ ─ ─ OrderRepresentation. Java │ └ ─ ─ OrderRepresentationService. JavaCopy the code

As you can see, we created a model package specifically to hold all domain objects related to the Order aggregation root; In addition, based on the same-type aggregation principle, a Command package and an Exception package are created to house the request class and exception class, respectively.

Application services, the face of the domain model

The concept of Use Case in UML refers to the basic logical unit of software to provide external business functions. In DDD, since the business is the first priority, it is natural to want the processing of the business to be visible, and DDD provides an abstraction layer called ApplicationService for this purpose. ApplicationService uses the facade pattern as the gateway to the domain model to provide business functions, just as the front desk of a hotel handles the different needs of customers.

When coding to implement business functions, two workflows are commonly used:

  • Bottom-up: Design the data model, such as the table structure of a relational database, before implementing the business logic. When I pair up with various programmers, I often hear the phrase “Let me design the fields of the database table first”. This approach prioritizes the focus on technical data models over business domain models, which is the opposite of DDD.
  • Top-down: Take a business requirement, work out the request data format with the customer, implement Controller and ApplicationService, implement the domain model (which is usually identified at this point), and implement persistence.

In DDD practice, a top-down implementation is a natural choice. The implementation of ApplicationService follows the simple principle that a business use case corresponds to a business method on ApplicationService. For example, the business requirement of “modifying the quantity of Product in Order” mentioned above is realized as follows:

Implement OrderApplicationService:

@Transactional
public void changeProductCount(String id, ChangeProductCountCommand command) {
    Order order = orderRepository.byId(orderId(id));
    order.changeProductCount(ProductId.productId(command.getProductId()), command.getCount());
    orderRepository.save(order);
}
Copy the code

OrderController calls OrderApplicationService:

@PostMapping("/{id}/products")
public void changeProductCount(@PathVariable(name = "id") String id, @RequestBody @Valid ChangeProductCountCommand command) {
    orderApplicationService.changeProductCount(id, command);
}

Copy the code

At this point, order.changeProductCount() and OrderRepository.save () are not necessary to be implemented, but the business processing framework consisting of OrderController and OrderApplicationService is already in place.

As you can see, the modified Order in the amount of the Product case of OrderApplicationService. ChangeProductCount () method only few of 3 lines of code, however, There are many subtleties to such a simple ApplicationService.

ApplicationService should follow the following principles:

  • Business methods and business use cases correspond one to one: I’ve already covered them.
  • Business methods correspond to transactions one by one: that is, each business method constitutes an independent transaction boundary, and in this case,OrderApplicationService.changeProductCount()Method marked with Spring@TransactionalAnnotation, indicating that the entire method is encapsulated in a transaction.
  • The business logic itself should not be contained: the business logic should be implemented in the domain model, or more specifically in the aggregate root, in this case,order.changeProductCount()Methods are where the real business logic is implemented, and ApplicationService is invoked only as a proxyorder.changeProductCount()Therefore, ApplicationService should be a very thin layer.
  • UI or communication protocol independent: ApplicationService is positioned not as the facade of the entire software system, but as the facade of the domain model, meaning that ApplicationService should not deal with technical details such as UI interactions or communication protocols. In this case, the Controller, as the ApplicationService caller, is responsible for handling the communication protocol (HTTP) and the direct interaction with the client. This approach makes ApplicationService universal, meaning that no matter whether the final caller is an HTTP client, an RPC client, or even a Main function, the domain model can be accessed through ApplicationService.
  • Accept raw data types: ApplicationService is the caller to the domain model, for whom the implementation details of the domain model should be a black box, so ApplicationService should not refer to objects in the domain model. In addition, the data in the request object accepted by ApplicationService only describes the business request itself and should be as simple as possible to meet the business requirements. Therefore, ApplicationService typically deals with some primitive data types. In this case,OrderApplicationServiceThe accepted Order ID is a primitive Java String that is encapsulated as the Repository in the domain model when calledOrderIdObject.

Business carrier – aggregation root

In a down-to-earth way, Aggreate roots (AR) are the most important domain objects in the software model that exist as nouns, such as Order and Product in this article’s sample project. For another example, for a Member management system, the Member is an aggregation root; For an Expense system, Expense is an aggregation root; For an insurance system, a Policy is an aggregation root. The aggregate root is the primary business logic carrier around which all tactical implementations in DDD revolve.

However, not all nouns in the domain model can be modeled as aggregate roots. Aggregation, as the name implies, requires highly cohesive concepts in a field to be brought together to form a whole. As to which concepts come together, we need to have a deep understanding of the business itself, which is why DDD emphasizes the need for development teams to work with domain experts. The event storm modeling activity that has become popular in recent years is intended to give us a comprehensive understanding of the business in the domain by listing all the events that happen in the domain, so as to identify the aggregation root.

For the “Update number of Products in Order” use case, the aggregate root Order is implemented as follows:

public void changeProductCount(ProductId productId, int count) {
    if (this.status == PAID) {
        throw new OrderCannotBeModifiedException(this.id);
    }

    OrderItem orderItem = retrieveItem(productId);
    orderItem.updateCount(count);
    this.totalPrice = calculateTotalPrice();
}

private BigDecimal calculateTotalPrice() {
    return items.stream()
            .map(OrderItem::totalPrice)
            .reduce(ZERO, BigDecimal::add);
}


private OrderItem retrieveItem(ProductId productId) {
    return items.stream()
            .filter(item -> item.getProductId().equals(productId))
            .findFirst()
            .orElseThrow(() -> new ProductNotInOrderException(productId, id));
}
Copy the code

In this case, the items (orderItems) and the totalPrice (totalPrice) in the Order are closely related, and a change in orderItems directly results in a change in totalPrice, so the two should naturally converge under the Order. In addition, the change in totalPrice is a necessary result of the change in orderItems. This causality is driven by the business. To ensure this “necessity”, we need to implement both “cause” and “effect” in order.changeProductCount (). That is, the aggregation root should ensure consistency across the business. In DDD, business uniformity is called Invariants.

Remember the “cohesion defying suspense” mentioned above? The business on Order was called as follows:

. order.changeProductCount(ProductId.productId(command.getProductId()), command.getCount()); order.updateTotalPrice(); .Copy the code

To implement the “update quantity of Product in Order” business function, two public methods on the Order, changeProductCount() and updateTotalPrice(), are called. While this approach also implements the business logic correctly, it places the responsibility of ensuring business consistency to the caller of the Order (the Controller above) rather than to the Order itself, The caller needs to make sure that the updateTotalPrice() method must be called after changeProductCount() is called, both as a leak of the business logic in Order and as a non-obligation for the caller, which Order should be.

The pursuit of cohesion naturally extends beyond the boundary of the polymerization root. In the strategic design of DDD, we have broken up a large software system into different “modules” by dividing it into bounded contexts, so it is much easier to talk about cohesion in a bounded context than in a mud-ball system.

Design for aggregate roots needs to be wary of God Objects, which are large, comprehensive domain objects that implement all business functions. There is a seemingly plausible logic behind god objects: since we want cohesion, let’s bring together all related things, such as a single Product class for all business scenarios, including orders, logistics, invoices, and so on. This mechanical way of looking cohesive is actually the opposite of that. To solve such problems, we still need to resort to bounded contexts. Different bounded contexts use their own Ubiquitous languages, which require that a business concept should not be ambiguous. Under this principle, different bounded contexts may have their own Product class, although they have the same name. It’s a different business.

In addition to cohesion and consistency, polymeric roots have the following characteristics:

  • The implementation of the aggregate root should be framework agnostic: since DDD emphasizes the separation of business and technical complexity, the aggregate root, the primary vehicle for the business, should have as few references to technical framed-level facilities as possible, preferably POJOs. Imagine if your project needed to migrate from Spring to Play, and you could confidently tell your boss that you could just copy the core Java code. Alternatively, many times the technical framework gets a “big step” upgrade that changes the API in the framework and no longer supports backward compatibility, in which case we can survive the framework upgrade if our domain model is framework-independent.
  • References between aggregate roots are done by ID: with properly designed aggregate root boundaries, a business use case updates only one aggregate root, so what good is it to reference the whole of the other aggregate root in that aggregate root? In this article’s example, oneOrderUnder theOrderItemRefer to theProductIdNot the whole thingProduct.
  • All changes within the aggregate root must be made through the aggregate root: to ensure consistency of the aggregate root and to avoid leakage of internal logic from the aggregate root, the client can only use the entire aggregate root as a unified call entry.
  • If a transaction needs to update more than one aggregate root, first think about whether your aggregate root boundary handling is wrong, because it is usually not the case that a transaction updates more than one aggregate root if it is properly designed. If this is indeed a business requirement, consider introducing a messaging mechanism and event-driven architecture that ensures that a transaction updates only one aggregate root, and then asynchronously updates the other aggregate roots via messaging.
  • Aggregates should not refer to infrastructure.

  • Outsiders should not hold data structures inside the aggregate root.
  • Use small aggregations whenever possible.

Entity vs value object

There is an Entity and a Value Object in the software model. This division is not really exclusive to DDD, but in DDD we emphasize the distinction between the two.

Entity objects represent objects that have a lifetime and have globally unique identifiers (ids), such as Order and Product in this article, while value objects represent non-unique identifiers used for descriptive purposes, such as Address objects.

An aggregate root must be an entity object, but not all entity objects are aggregate roots, and aggregate roots can have other child entity objects. The ID of the aggregate root is globally unique in the entire software system, while the ID of the subentity object beneath it needs to be unique under a single aggregate root. In this article’s sample project, OrderItem is a child entity object under the aggregate root Order:

public class OrderItem {
    private ProductId productId;
    private int count;
    private BigDecimal itemPrice;
}
Copy the code

As you can see, although OrderItem uses ProductID as its ID, we do not enjoy the global uniqueness of ProductID at this time. In fact, multiple orders can contain OrderItem of the same ProductID, that is, multiple orders can contain the same product.

Distinguish between entities and value object of a very important principle is judged according to the equality, equality of entity object is accomplished by ID, for two entities, if they all attributes are the same, but the ID is different, so they are two different entities, like a pair of identical twins, they are still two different natural person. For value objects, equality is determined through property fields. For example, the Address object under an order is a typical value object:

public class Address  {
    private String province;
    private String city;
    private String detail;

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Address address = (Address) o;
        return province.equals(address.province) &&
                city.equals(address.city) &&
                detail.equals(address.detail);
    }

    @Override
    public int hashCode() {
        return Objects.hash(province, city, detail);
    }

}
Copy the code

In the equals() method of addresses, the equality of two addresses is determined by determining all the attributes (province, city, detail) contained in the Address.

Value objects are Immutable, meaning that once a value object is created, it cannot be changed. To change it, a new value object must be created to replace the original one. For example, the sample project has a business requirement:

In case the order is not paid, you can modify the shipping address details of the order.

Since Address is an object in the Order aggregate root, changes to Address can only be done with Order, where the changeAddressDetail() method is implemented:

public void changeAddressDetail(String detail) {
    if (this.status == PAID) {
        throw new OrderCannotBeModifiedException(this.id);
    }

    this.address = this.address.changeDetailTo(detail);
}
Copy the code

As you can see, by calling the address.changeDetailto () method, we get a new address object and assign the new address object as a whole to the Address attribute. The implementation of address.changeDetailto () is as follows:

public Address changeDetailTo(String detail) {
    return new Address(this.province, this.city, detail);
}
Copy the code

The changeDetailTo() method recreates an Address object using the new Address detail and the unchanged province and city.

The immutability of value objects makes the logic of the program much simpler. You don’t have to maintain complex state information. You create it when you need it and throw it away when you don’t, making value objects look like passers-by in the program. A favored approach in DDD modeling is to model business concepts as value objects as much as possible.

For OrderItem, this article models OrderItem as an entity object because our business needs to modify the quantity of OrderItem, meaning it has a life cycle. However, if there is no such business requirement, it would be more appropriate to model OrderItem as a value object.

In addition, it should be noted that the division of entity and value object is not invariable, but should be defined according to the bounding context. The same business noun may be an entity in one bounding context and a value object in another bounding context. For example, an Order Order should be modeled as an entity in the procurement context, but can be modeled as a value object in the logistics context.

The home of the aggregate root – the repository

In layman’s terms, a Repository is used to persist aggregate roots. Technically, Repository plays a similar role to DAO, but DAO is designed as a thin wrapper around a database, whereas Repository is more domaine-oriented. In addition, of all domain objects, only the aggregation root “deserves” to have Repository; daOs do not have this constraint.

The repository that implements Order is as follows:

public void save(Order order) { String sql = "INSERT INTO ORDERS (ID, JSON_CONTENT) VALUES (:id, :json) " + "ON DUPLICATE KEY UPDATE JSON_CONTENT=:json;" ; Map<String, String> paramMap = of("id", order.getId().toString(), "json", objectMapper.writeValueAsString(order)); jdbcTemplate.update(sql, paramMap); } public Order byId(OrderId id) { try { String sql = "SELECT JSON_CONTENT FROM ORDERS WHERE ID=:id;" ; return jdbcTemplate.queryForObject(sql, of("id", id.toString()), mapper()); } catch (EmptyResultDataAccessException e) { throw new OrderNotFoundException(id); }}Copy the code

In OrderRepository, we define only the save() and byId() methods to save/update the aggregate root and get the aggregate root byId, respectively. These two methods are the most common in Repository, and some DDD practitioners even argue that a pure Repository should contain only these two methods.

Reading this, you may be wondering why there are no update and query methods in OrderRepository. In fact, Repository’s role is to provide an aggregate root to the domain model, like a “container” for the aggregate root. The container itself does not care whether the client adds or updates the aggregate root. You give an aggregate root object, Repository is only responsible for synchronizing its state from the computer’s memory to the persistence mechanism. In this sense, Repository only needs a method like save() to do the synchronization. Of course, this is the design result from the conceptual point of view. At the technical level, new and update needs to be treated differently. For example, SQL statements can be divided into INSERT and update, but we hide such technical details in the save() method and the customer does not need to know these details. In this example, we use MySQL’s ON DUPLICATE KEY UPDATE feature to process both new and updated operations to the database. Of course, we can also determine programmatically whether the aggregate root already exists in the database, if so update, otherwise insert. In addition, persistence frameworks such as Hibernate automatically provide saveOrUpate() methods that can be used directly to persist aggregate roots.

There is nothing unreasonable about implementing queries in Repository, but the evolution of the project can lead to a lot of query code in Repository that obscures the original purpose of Repository. In fact, reading and writing are two very different processes in DDD, and my recommendation is to keep them as separate as possible so that the query function can be separated from Repository, as I’ll explain in more detail below.

In this case, we use Spring’s JdbcTemplate and JSON format persistence Order aggregation root. Repository is not bound to any persistence mechanism. The functional “interface” exposed by an abstracted Repository is always to provide an aggregate root object to the domain model, acting as the “home of the aggregate root”.

Ok, so to recap, we used the business requirement “update the number of products in Order” as an example, and talked about application services, aggregation roots, and repositories. The process for this business requirement represents the most common and typical form of DDD handling business requirements:

The application service, acting as the overall coordinator, retrieves the aggregate root from the repository, then invokes the business methods in the aggregate root, and finally invokes the repository again to hold the aggregate root.

The process diagram is as follows:

The pillar of creation — factory

A little refinement tells us that software writes either modify existing data or create new data. The answer to the former has already been covered in DDD, so let’s talk about how to create a new aggregate root in DDD.

The creation of an aggregate root is usually done through the Factory pattern in the design pattern, which has both the benefit of the Factory pattern itself and the effect of a Factory in DDD that exposes the creation logic of the aggregate root.

The process of creating an aggregate root can be simple or complex, sometimes just by calling the constructor directly, and sometimes there is a complex construction process, such as calling other systems to get data. Generally speaking, Factory is implemented in two ways:

  • The Factory method is implemented directly in the aggregation root and is often used for simple creation processes
  • A separate Factory class for a complex creation process or one where the creation logic does not fit at the aggregation root

Let’s first demonstrate the simple Factory method. In the sample order system, one business use case is “create Product” :

Create Product. Attributes include name, description, and price. ProductId is the UUID

Implement factory method create() in Product class:

public static Product create(String name, String description, BigDecimal price) {
    return new Product(name, description, price);
}

private Product(String name, String description, BigDecimal price) {
    this.id = ProductId.newProductId();
    this.name = name;
    this.description = description;
    this.price = price;
    this.createdAt = Instant.now();
}
Copy the code

Here, the create() method in Product does not contain the creation logic, but instead delegates the creation directly to the Product constructor. You may think this create() method is superfluous, but the original intention is that we want to highlight the creation logic of the aggregate root. Constructors themselves are very technical and need to be used anywhere that involves creating new objects in computer memory, whether the initial reason for creation is business, loading from a database, or deserializing from JSON data. Therefore, programs often have multiple constructors for different scenarios, and to distinguish business creation from technical creation, we introduced the create() method to represent the business creation process.

The Factory “create Product” is designed to be simple. Let’s look at another example: “Create Order” :

Create an Order that contains the Product selected by the user and its quantity. OrderId must be retrieved by calling a third party’s OrderIdGenerator

The OrderIdGenerator here is an object that has the nature of a service (that is, a domain service below), and in DDD the aggregation root usually does not refer to other service classes. In addition, calling OrderIdGenerator to generate the ID should be a business detail that, as mentioned earlier, should not be placed in ApplicationService. Order can be created using the Factory class:

@Component public class OrderFactory { private final OrderIdGenerator idGenerator; public OrderFactory(OrderIdGenerator idGenerator) { this.idGenerator = idGenerator; } public Order create(List<OrderItem> items, Address address) { OrderId orderId = idGenerator.generate(); return Order.create(orderId, items, address); }}Copy the code

Necessary compromise — domain services

As mentioned earlier, the aggregate root is the primary vehicle for business logic, meaning that the implementation code for business logic should be placed as much as possible within the aggregate root or within the boundaries of the aggregate root. Sometimes, however, some business logic does not fit at the aggregation root, as was the case with the OrderIdGenerator above, and in this “out of necessity” case, Domain Services are introduced. To start with an example, there are the following business use cases for payment of Order:

The Order is paid through the payment gateway OrderPaymentService.

In OrderApplicationService, call the domain service OrderPaymentService directly:

@Transactional
public void pay(String id, PayOrderCommand command) {
    Order order = orderRepository.byId(orderId(id));
    orderPaymentService.pay(order, command.getPaidPrice());
    orderRepository.save(order);
}
Copy the code

Then implement OrderPaymentService:

public void pay(Order order, BigDecimal paidPrice) {
    order.pay(paidPrice);
    paymentProxy.pay(order.getId(), paidPrice);
}
Copy the code

The PaymentProxy here is similar to OrderIdGenerator and does not fit in an Order. As you can see, in OrderApplicationService, instead of calling the business method in Order directly, we call OrderPaymentService.pay() first, The business details such as calling the payment gateway paymentproxy.pay () are then completed in orderPaymentService.pay ().

In general practice, we write the Service class. In fact, these Servcie classes combine the DDD ApplicationService and DomainService together. For example, this is the case for OrderService in the “Implementation based on Service + Anaemic Model” section. In DDD, ApplicationService and DomainService are two very different concepts. The former is a required DDD component, and the latter is a compromise, so the application should have as little DomainService as possible.

The Command object

In general, writes in DDD do not need to return data to the client. In some cases (such as creating a new aggregate root) an aggregate root ID can be returned, which means that ApplicationService or write methods in the aggregate root usually return void. For example, for OrderApplicationService, the methods are signed as follows:

public OrderId createOrder(CreateOrderCommand command) ;
public void changeProductCount(String id, ChangeProductCountCommand command) ;
public void pay(String id, PayOrderCommand command) ;
public void changeAddressDetail(String id, String detail) ;
Copy the code

As you can see, in most cases we use the suffix for the Command object to ApplicationService, such as CreateOrderCommand and ChangeProductCountCommand. Command means Command, that is, a write operation represents a Command operation from the outside to the domain model. In fact, technically, a Command object is just a type of DTO object that encapsulates the request data sent by the client. All write operations received in the Controller need to be wrapped by Command. After the Controller can unpack Command when Command is relatively simple (such as only 1-2 fields), Pass the data directly to the ApplicationService, such as changeAddressDetail(); When a Command has a large number of data fields, you can pass the Command object directly to ApplicationService. Of course, this is not a strict principle to follow in DDD. For example, passing all commands from Controller to ApplicationService, regardless of Command simplicity, is not a problem. It is more of a coding convention. It is important to note, however, that the previous statement that ApplicationService needs to accept primitive data types instead of objects in the domain model means that the Command object should also contain primitive data types.

Another advantage of using Command objects uniformly is that by looking for all objects with the Command suffix, we can get an overview of the business functions that a software system provides externally.

To summarize, above we mainly focus on the realization of software “write operation” in DDD, and talked about three scenarios, respectively:

  • Complete the business request through the aggregate root
  • The aggregation root is created through Factory
  • Complete service requests using DomainService

The above three scenarios roughly cover the basic aspects of DDD business write operations, which can be summarized in three sentences: create aggregate root through Factory; Business logic is done within aggregation root boundaries first; Business logic that does not fit in the aggregate root is considered for DomainService.

Read operations in DDD

The read model in software is very different from the write model. The business logic we usually talk about is more concerned with the write operation, while the read operation is more concerned with how to return the appropriate data presentation to the customer.

In DDD write operations, we need to code strictly according to the “application service -> aggregate root -> repository” structure, while in read operations, using the same structure as the write operation sometimes does not benefit, but rather makes the whole process cumbersome. There are three read operations:

  • Read operations based on domain models
  • Data model-based read operations
  • CQRS

First, no matter what type of read operation, the rule is that objects in the domain model cannot be returned directly to the client, because then the interior of the domain model is exposed to the outside world, and changes to the domain model directly affect the client. Therefore, in DDD we usually create a model for data presentation specifically for read operations. In the write operation, we request data unification through the Command suffix, and in the read operation, we show data unification through the Representation suffix, which is also the “R” in REST.

Read operations based on domain models

This method combines the read model and the write model. The domain model is first acquired from the repository and then converted into a Representation object. This method is also widely used today.

@Transactional(readOnly = true)
public OrderRepresentation byId(String id) {
    Order order = orderRepository.byId(orderId(id));
    return orderRepresentationService.toRepresentation(order);
}
Copy the code

We first get the Order aggregate root object via orderRepository.byid (). Then call orderRepresentationService. ToRepresentation () converts the Order to show the object OrderRepresentation, OrderRepresentationService. ToRepresentation () implementation is as follows:

public OrderRepresentation toRepresentation(Order order) {
    List<OrderItemRepresentation> itemRepresentations = order.getItems().stream()
            .map(orderItem -> new OrderItemRepresentation(orderItem.getProductId().toString(),
                    orderItem.getCount(),
                    orderItem.getItemPrice()))
            .collect(Collectors.toList());

    return new OrderRepresentation(order.getId().toString(),
            itemRepresentations,
            order.getTotalPrice(),
            order.getStatus(),
            order.getCreatedAt());
}
Copy the code

The advantage of this approach is that it is very straightforward and does not require the creation of a new data access mechanism. However, the disadvantages are also obvious: First, the read operation is completely bound to the boundary division of the aggregation root. For example, if the client needs to obtain the Order and the Product contained in it at the same time, we need to load the Order aggregation root and the Product aggregation root into the memory for conversion, which is cumbersome and inefficient. In read operations, data is usually returned based on different query conditions, such as the date of the Order query or the name of the Product query. As a result, too much query logic is processed in the Repository and becomes increasingly complex. It is also moving away from what Repository is supposed to do.

Data model-based read operations

This approach bypasses the repository and aggregation and reads the data the client needs directly from the database, where the write and read operations share only the database. For example, for “access to the Product list” interface, through a dedicated ProductRepresentationService directly read data from the database:

 @Transactional(readOnly = true)
public PagedResource<ProductSummaryRepresentation> listProducts(int pageIndex, int pageSize) {
    MapSqlParameterSource parameters = new MapSqlParameterSource();
    parameters.addValue("limit", pageSize);
    parameters.addValue("offset", (pageIndex - 1) * pageSize);

    List<ProductSummaryRepresentation> products = jdbcTemplate.query(SELECT_SQL, parameters,
            (rs, rowNum) -> new ProductSummaryRepresentation(rs.getString("ID"),
                    rs.getString("NAME"),
                    rs.getBigDecimal("PRICE")));

    int total = jdbcTemplate.queryForObject(COUNT_SQL, newHashMap(), Integer.class);
    return PagedResource.of(total, pageIndex, products);
}
Copy the code

Then return directly in Controller:

@GetMapping
public PagedResource<ProductSummaryRepresentation> pagedProducts(@RequestParam(required = false, defaultValue = "1") int pageIndex,
                                                                 @RequestParam(required = false, defaultValue = "10") int pageSize) {
    return productRepresentationService.listProducts(pageIndex, pageSize);
}
Copy the code

As you can see, the whole process does not use the ProductRepository and Product, but the SQL access to new as ProductSummaryRepresentation object data directly.

The advantage of this approach is that the process of read operations is not limited to the domain model, but can directly obtain the required data based on the requirements of the read operation itself, which simplifies the whole process on the one hand, and greatly improves the performance on the other hand. However, because the read and write operations share a database, and the database is created primarily with a structure corresponding to the aggregate root, the read operation is still constrained by the data model of the write operation. But this approach is a good compromise and has been advocated by Microsoft. See the Microsoft website for more details.

CQRS

CQRS(Command Query Responsibility Segregation) means Segregation of Command Query responsibilities. Commands can be interpreted as write operations, while queries can be interpreted as read operations. Unlike “data model-based reads,” in CQRS write and read operations use different databases, and data is synchronized from the write model database to the read model database, usually in the form of domain events to synchronize change information.

In this way, the read operation can design the data structure according to its own needs, without being constrained by the data structure of the write model. CQRS is a big topic in and of itself and is beyond the scope of this article, so readers can explore it on their own.

At this point, read operations in DDD can be roughly implemented in three ways:

conclusion

This paper mainly introduces the concepts of application service, aggregation, repository and factory in DDD and the coding practices related to them. Then it focuses on the realization of software read and write operations in DDD. The three scenarios of write operations are as follows:

  • Business requests are completed through an aggregate root, which is a typical way for DDD to complete business requests
  • The aggregation root is created using Factory to create an aggregation root
  • Business requests are made through DomainService, which is considered to be placed in the aggregate root only when appropriate

For read operations, three modes are also provided:

  • Read operations based on the domain model (read and write operations are mixed together, not recommended)
  • Data model-based reads (bypassing aggregate roots and repositories and returning data directly, recommended)
  • CQRS(separate database for read and write operations)

The above “3 read and 3 write” basically covers the day-to-day development of business functions for programmers. DDD is that simple, isn’t it?