14 How to Design a Technical Backend That Supports Ddd

14 How to design a technical backend that supports DDD #

The biggest challenge in implementing DDD is how to design a technology architecture that supports DDD. Many teams encounter various problems during the coding phase, such as not accurately grasping the layered architecture of DDD or writing messy code that requires frequent data transformation between various data objects, making future changes extremely difficult.

Therefore, a strong technical middle platform is needed to simplify the design and implementation of DDD and solve the “last mile” problem. Only in this way can DDD truly be implemented in a project.

Traditional DDD Architecture Design #

Drawing 1.png

Usually, the architecture design of software projects that support domain-driven development is as shown in the above diagram.

  • The presentation layer is the UI layer of the front-end, which interacts with the back-end application layer through the network.
  • The application layer is similar to the MVC layer and is mainly used for front-end and back-end interactions. After receiving user requests, it will invoke services in the domain layer, i.e., the Service layer.
  • In the domain layer, user requests are first received by the Service layer, and then, during the process of performing business operations, domain objects are used as parameters (implementation of anemic models) or the corresponding methods in domain objects are called (implementation of rich models). In terms of domain object design, it can be an entity, a value object, or they can be made into an aggregate (if there is a whole-part relationship between multiple domain objects).
  • Finally, domain objects are persisted to the database through repositories. Data is read from and assembled and restored to domain objects using factories.

These are the ways domain-driven development is applied to software design. In terms of architecture layers, the design of DDD repositories and factories is between the business domain layer and the infrastructure layer, with interfaces in the business domain layer and implementations in the infrastructure layer. The infrastructure layer of DDD is equivalent to the underlying technology architecture that supports DDD, and uses various technology frameworks to support software systems to accomplish functions other than domain-driven development.

1.png

However, with traditional software systems designed using DDD, various data structure transformations need to be performed between different layers:

  • First, the data structure on the front-end is in JSON format, and it needs to be converted to a Data Transfer Object (DTO) when transmitted to the back-end data access layer.
  • Then, when the application layer calls the domain layer, it needs to convert the DTO to a domain object (DO).
  • Finally, when data is persisted to the database, the DO needs to be converted to a persistent object (PO).

In this process, a large amount of code needs to be written for data transformation, which undoubtedly increases the workload of software development and the maintenance cost of future changes. Therefore, can we consider the design mentioned in the previous lecture to unify the data structures of each layer?

Drawing 5.png

In addition, in traditional software systems designed using DDD, each functional module needs to have its own repository and factory. For example, the order module has an OrderRepository and an OrderFactory, and the inventory module has an InventoryRepository and an InventoryFactory. Although different modules have implemented different business logics in their repositories and factories, they have also formed a large amount of repetitive code. This problem is similar to the problem with DAO mentioned earlier. Can we simplify this by designing a unified repository and factory through configuration and modeling? If so, what is the relationship between the repository, factory, and DAO? Based on thinking about these issues, I propose the design of a unified data modeling, embedded aggregates implementation, and generic repositories and factories to simplify DDD business development. Therefore, the following architecture design was carried out.

Design of Generic Repositories and Factories #

Drawing 7.png The design of this application differs from the architecture design discussed in the previous lesson, with the only difference being the replacement of the single Dao with a generic repository and factory. In other words, compared to the Dao, the repository in DDD extends some new functionality based on the Dao.

  • For example, when loading or querying orders, not only do we need to query the order table, but we also need to fill in the order details, customer information, and product information related to the order and assemble them into an order object. In this process, querying the order is the functionality of the Dao, but other operations such as filling and assembling are the functionality extensions performed by the repository based on the Dao.
  • Similarly, when saving an order, not only do we need to save the order table, but we also need to save the order details table and put them in the same transaction. Saving the order table is the original functionality of the Dao, saving the order details table, and adding the transaction are the functionality extensions performed by the repository based on the Dao.

This is the relationship between the repository and the Dao in DDD.

Based on this extension relationship, how should we design this generic repository? If you are familiar with design patterns, you may think of the “Decorator pattern”. The purpose of the “Decorator pattern” is to perform “transparent functionality extension” on the original functionality. This “transparent functionality extension” can extend the original functionality without affecting the original client program, allowing the client program to implement new functionality without modifying any code, thereby reducing the cost of maintenance for changes. Therefore, the “generic repository” is designed like this.

Drawing 9.png

That is, on the basis of the original BasicDao and BasicDaoImpl, a generic repository Repository is added. The Repository is designed as a decorator, which is also an implementation class of the BasicDao interface, and it refers to the BasicDao through an attribute variable. When using it, you can extend the DDD functionality by encapsulating the Repository based on BasicDaoImpl. Therefore, when injecting the Dao into the Service:

  • If DDD is not used, inject BasicDaoImpl as before;
  • If you need to use DDD, inject Repository.

The configuration is as follows:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" ...>

 <description>The application context for orm</description>

 <bean id="basicDao" class="com...impl.BasicDaoJdbcImpl"></bean>

 <bean id="redisCache" class="com...cache.RedisCache"></bean>

 <bean id="repository" class="com...RepositoryWithCache">

  <property name="dao" ref="basicDao"></property>

  <property name="cache" ref="redisCache"></property>

 </bean>

 <bean id="product" class="com.demo2...impl.ProductServiceImpl">

  <property name="dao" ref="repository"></property>

 </bean>
<bean id="supplier" class="com.demo2...impl.SupplierServiceImpl">
  <property name="dao" ref="basicDao"></property>
</bean>

<bean id="productQry" class="com.demo2...AutofillQueryServiceImpl">
  <property name="queryDao">
    <bean class="com.demo2.support.dao.impl.QueryDaoMybatisImpl">
      <property name="sqlMapper" value="com.demo2...dao.ProductMapper.query"></property>
    </bean>
  </property>
  <property name="dao" ref="basicDao"></property>
</bean>

</beans>

In this configuration, it can be seen that the Repository has a property Dao configured as BasicDao. This allows the Repository to access the database through BasicDao. At the same time, two generic repositories, Repository and RepositoryWithCache, are implemented here. If the latter is configured, caching functionality can be achieved.

In the example above, Product is configured with Repository as the Dao. This means that when loading Product by ID, the associated Supplier will be loaded into the Product object. Meanwhile, productQry is configured with AutofillQueryServiceImpl as the queryDao, so after querying for product information, the associated Supplier will be automatically populated.

How does the generic repository guide the association between Product and Supplier? The key lies in the configuration in the vObj.xml file:

<?xml version="1.0" encoding="UTF-8"?>

<vobjs>

  <vo class="com.demo2.trade.entity.Product" tableName="Product">

    <property name="id" column="id" isPrimaryKey="true"></property>

    <property name="name" column="name"></property>

    <property name="price" column="price"></property>

    <property name="unit" column="unit"></property>

    <property name="classify" column="classify"></property>

    <property name="supplier_id" column="supplier_id"></property>

    <join name="supplier" joinKey="supplier_id" joinType="manyToOne"       class="com.demo2.trade.entity.Supplier"></join>

  </vo>

  <vo class="com.demo2.trade.entity.Supplier" tableName="Supplier">

    <property name="id" column="id" isPrimaryKey="true"></property>

    <property name="name" column="name"></property>

  </vo>

</vobjs>

In Product, a join tag is added to specify the association between domain objects. The joinKey attribute is set to “supplier_id”, which means that the supplier_id attribute in the Product object is used to associate with the key value in Supplier. The joinType attribute represents the association type, which supports three types of associations: oneToOne, manyToOne, and oneToMany. However, for performance reasons, manyToMany associations are not supported. When the type is oneToMany, the populated value is a collection, so the domain object should also have a collection property. For example, in Customer, there is a oneToMany relationship with Address, so the domain object is designed as follows:

/**
 * The customer entity
 * @author fangang
 */
public class Customer extends Entity<Long> {

  ......

  private List<Address> addresses;

  /**
   * @return the addresses
   */
  public List<Address> getAddresses() {
    return addresses;
  }
/**
 * @param addresses the addresses to set
 */
public void setAddresses(List<Address> addresses) {
    this.addresses = addresses;
}

}


Therefore, configure the vObj.xml as follows:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<vobjs>
  <vo class="com.demo2.customer.entity.Customer" tableName="Customer">
    <property name="id" column="id" isPrimaryKey="true"></property>
    <property name="name" column="name"></property>
    <property name="sex" column="sex"></property>
    <property name="birthday" column="birthday"></property>
    <property name="identification" column="identification"></property>
    <property name="phone_number" column="phone_number"></property>
    <join name="addresses" joinKey="customer_id" joinType="oneToMany" isAggregation="true" class="com.demo2.customer.entity.Address"></join>
  </vo>
  <vo class="com.demo2.customer.entity.Address" tableName="Address">
    <property name="id" column="id" isPrimaryKey="true"></property>
    <property name="customer_id" column="customer_id"></property>
    <property name="country" column="country"></property>
    <property name="province" column="province"></property>
    <property name="city" column="city"></property>
    <property name="zone" column="zone"></property>
    <property name="address" column="address"></property>
    <property name="phone_number" column="phone_number"></property>
  </vo>
</vobjs>

In this way, when loading and querying Customer, the built-in system will also load its associated Address. During the loading process, the Dao will query the data from the database and then pass the retrieved Customer and multiple Addresses to the common factory for assembly. If the configuration is RepositoryWithCache, it will first check if the customer is in the cache before loading. If not, it will query from the database.

Built-in Aggregation Functionality #

Aggregation is a very important concept in Domain-Driven Design (DDD), which represents the relationship between a whole and its parts in the real world. For example, Order and OrderItem have a whole-part relationship. When loading an order, its order details should also be loaded, and when saving an order, both the order and its order details should be saved in the same transaction. When designing a technology platform that supports DDD, the design and implementation of aggregations should be simplified, so that business developers don’t have to write a lot of code every time, but can achieve aggregations through configuration.

For example, if there is an aggregation relationship between Order and OrderItem, you can model it in vObj.xml by using the join tag to associate them and set the isAggregation attribute of the join tag to true. This way, when querying or loading an order, all its order details will be loaded, and when saving an order, its order details will be saved and put in the same transaction. The specific configuration is as follows:

<?xml version="1.0" encoding="UTF-8"?>
<vobjs>
  <vo class="com.demo2.order.entity.Customer" tableName="Customer">
    <property name="id" column="id" isPrimaryKey="true"></property>
    <property name="name" column="name"></property>
    <property name="sex" column="sex"></property>
    <property name="birthday" column="birthday"></property>
    <property name="identification" column="identification"></property>
    <property name="phone_number" column="phone_number"></property>
    <join name="addresses" joinKey="customer_id" joinType="oneToMany" isAggregation="true" class="com.demo2.order.entity.Address"></join>
  </vo>
  ...
</vobjs>
<vo class="com.demo2.order.entity.Address" tableName="Address">

   <property name="id" column="id" isPrimaryKey="true"></property>

   <property name="customer_id" column="customer_id"></property>

   <property name="country" column="country"></property>

   <property name="province" column="province"></property>

   <property name="city" column="city"></property>

   <property name="zone" column="zone"></property>

   <property name="address" column="address"></property>

   <property name="phone_number" column="phone_number"></property>

</vo>

<vo class="com.demo2.order.entity.Product" tableName="Product">

  <property name="id" column="id" isPrimaryKey="true"></property>

  <property name="name" column="name"></property>

  <property name="price" column="price"></property>

  <property name="unit" column="unit"></property>

  <property name="classify" column="classify"></property>

  <property name="supplier_id" column="supplier_id"></property>

</vo>

<vo class="com.demo2.order.entity.Order" tableName="Order">

   <property name="id" column="id" isPrimaryKey="true"></property>

   <property name="customer_id" column="customer_id"></property>

   <property name="address_id" column="address_id"></property>

   <property name="amount" column="amount"></property>

   <property name="order_time" column="order_time"></property>

   <property name="flag" column="flag"></property>

   <join name="customer" joinKey="customer_id" joinType="manyToOne" class="com.demo2.order.entity.Customer"></join>

   <join name="address" joinKey="address_id" joinType="manyToOne" class="com.demo2.order.entity.Address"></join>

   <join name="orderItems" joinKey="order_id" joinType="oneToMany" isAggregation="true" class="com.demo2.order.entity.OrderItem"></join>

</vo>

<vo class="com.demo2.order.entity.OrderItem" tableName="OrderItem">

   <property name="id" column="id" isPrimaryKey="true"></property>

   <property name="order_id" column="order_id"></property>

   <property name="product_id" column="product_id"></property>

   <property name="quantity" column="quantity"></property>

   <property name="price" column="price"></property>

   <property name="amount" column="amount"></property>

   <join name="product" joinKey="product_id" joinType="manyToOne" class="com.demo2.order.entity.Product"></join>

</vo>

</vobjs>

In this configuration, it can be seen that orders are associated not only with order details, but also with customer, customer address, and other information. However, there is no aggregation relationship between orders and customer, customer address, etc. When saving an order, there is no need to save or modify this information. Only the order details have an aggregation relationship with the order, so isAggregation=true should be added to the join tag for order details in the order configuration. This way, when saving an order, the order details are also saved and they are placed in the same transaction. With this design, the implementation of aggregation is simplified and it is handled by the underlying technology platform, independent of the business code. Therefore, the system can continuously optimize the design and implementation of aggregation through the underlying technology platform, reducing the cost of changes.

Summary #

In this lesson, by using a technical platform that supports DDD, many complex design implementations of DDD are encapsulated in a generic repository and factory, which are part of the underlying technology platform. This allows business development teams to focus more on domain modeling and configure the models according to certain rules to achieve DDD-based design and development. The underlying technology platform can then handle the corresponding data persistence and query loading based on these configurations.

At the same time, the above design simplifies system design by eliminating the need to convert data between JSON, TDO, DO, and PO. Instead, by following the specifications, JSON is designed to be consistent with DO, and DO is configured with the database to complete development. This reduces the amount of code and makes maintenance and changes easier in the future.

In addition, there is an interesting question from students: if I don’t want to load order details when querying orders, will it affect performance if the order details are loaded anyway? The answer is yes. Therefore, in the face of high concurrency in the future, rich clients should be used to reduce the number of front-end and back-end interactions. Therefore, in the design, as much data as possible should be loaded to the front end, so that more operations can be performed directly on the front end. This effectively reduces the number of interactions and reduces system pressure.

In the next lesson, we will further explore how to design microservices and technical platforms that support DDD.

Click here to view the source code on GitHub.