Switching to code: Reduce complexity, from abandoning the three-tier architecture to getting started with DDD

1. Introduction

Recently I noticed an application on a team project getting more and more complex as follows:

  • Poor code readability: complex calls between services and unclear processes
  • Modifying some services leads to a large number of test case failures, but it is difficult to find out the root cause of these test case failures quickly

With that in mind, I started looking for ways to reduce complexity, which led to this article on DDD.

1.1 Specific Problems

1.1.1 Macro perspective

At a macro level, the evolution of software architecture pattern has gone through three stages.

  • The first stage is single-machine architecture: using process-oriented design method, the system includes client UI layer and database two layers, using C/S architecture mode, the whole system around database driven design and development, and always start from the design of database and fields.
  • The second stage is the centralized architecture: the object-oriented design method is adopted. The system includes the business access layer, the business logic layer and the database layer, and adopts the classic three-layer architecture. Some applications also adopt the traditional SOA architecture. This architecture tends to make the system bloated and poor in scalability and elastic scalability.
  • The third stage is distributed microservice architecture: with the introduction of the concept of microservice architecture, centralized architecture is evolving towards distributed microservice architecture. Microservice architecture can realize the decoupling between applications and solve the problem of insufficient scalability and elastic scalability of single applications. We know that in the era of stand-alone and centralized architectures, systems analysis, design, and development tend to occur independently and in phases.

For example, in the process of system construction, we often see such A situation: A is responsible for putting forward requirements, B is responsible for requirement analysis, C is responsible for system design, and D is responsible for code implementation. Such A long process, with many people, is easy to lead to information loss. Finally, it is easy to lead to the inconsistency between requirements, design and code implementation. Often, after the software goes online, we find that many functions are not what we want, or the functions we make are too far from the requirements we put forward.

Moreover, in both standalone and centralized architectures, software cannot respond quickly to the rapid changes in requirements and business, thus missing the opportunity for development. At this point, the emergence of distributed micro-services is a bit timely meaning.

The above section, from Geek Time, points out that DDD is generally used for microservice design and fragmentation, but I think it is also recommended to split modules in individual applications so that your modules can be split instantly when needed — into a separate microservice. 4. In-process service, this is an open source, and implemented in production is a good case.

1.1.2 Microscopic Angle

The problem is simple: the code for a service must pile up and collect more and more business.

image

2. Introduction to DDD

Let’s start with a picture:

Start with the outermost layer — what is a domain? In plain English, it’s a confluence of problems. Here’s an example:

  • In the e-commerce domain of the e-commerce platform, you need to solve a series of problems:

    • User authentication
    • Mobile payment
    • The order
    • offer
    • .

As you can see, domains are presented as a set of business domain problems.

In different domains, the abstract form of the same data entity is often different. For example, books in Bookstore focus on price in sales, quantity in inventory in storage, and introduction of books in commodity display.

2.1 Context Boundaries

Inside, we should see the bounded context. In fact, this translation is not good. The original text called bounded context, which is more appropriate. Essentially, it defines the boundary. More specifically, it is used to encapsulate common language and domain objects, provide context, and ensure that some terms, business-related objects, etc. (common language) in the domain have a definite meaning without ambiguity.

2.2 the aggregation

Next, we see aggregation. Aggregation is composed of entities and value objects closely related to business and logic. Aggregation is the basic unit of data modification and persistence. Each aggregation corresponds to a warehouse to realize data persistence.

An aggregation has an aggregation root and context boundary that defines which entities and value objects should be contained within the aggregation based on the principle of a single business responsibility and high cohesion, while the boundary between the aggregations is loosely coupled. Microservices designed in this way are naturally “highly cohesive and low coupled”.

What are the convergent roots?

The main purpose of the aggregation root is to avoid aggregation and data inconsistencies between entities due to the lack of uniform business rule control for complex data models.

In the traditional data model, every entity is peer, and uncontrolled invocations and data modifications by entities can lead to inconsistencies in data logic between entities. However, if the lock method is adopted, the software complexity will be increased and the system performance will be reduced.

If an aggregate is an organization, then the aggregate root is the person in charge of the organization. The aggregation root, also known as the root entity, is not only the entity but also the manager of the aggregation.

First of all, as the entity itself, it owns the attributes and business behaviors of the entity and realizes its own business logic.

Second, it acts as the manager of the aggregation, coordinating entities and value objects within the aggregation to work together to accomplish common business logic according to fixed business rules.

Finally, between aggregations, it is also the external interface person of aggregation, and accepts external tasks and requests in the way of aggregation root ID association, so as to realize business collaboration between aggregations within the context. That is, the aggregate root ID is used to associate references between aggregates. If you need to access other aggregated entities, you need to access the aggregate root first and then navigate to the aggregate internal entity. External objects cannot directly access the aggregate internal entity.

2.3 Entities and Value objects

There is a class of objects in DDD that have unique identifiers that remain consistent through various state changes. What matters to these objects is not their attributes, but their continuity and identity, which spans and extends beyond the lifecycle of the software. We call such objects entities. It’s like a database with rows of business data with the same ID.

A value object is less important because it is a set of attributes used to describe an entity. Many implementations in the system will be implemented in JSON, such as ZStack 7. Label system.

And just to make it easier to understand, let me make a little summary here. Entities and value object are abstract the purpose of the aggregate number of attributes to simplify the design and communication, with the a layer of abstraction, we in the use of personnel entity, won’t produce ambiguity, the reference address value object, don’t have to list all of its attributes, in the same limit context, greatly reduce misunderstanding, to reduce the deviation, the difference between the two is as follows:

  1. Both of them are formed by attribute clustering. Entities are unique, but value objects are not. In the bounded context of this case, people have uniqueness. Once a person is managed by the system, it is endowed with the ability to be uniquely identified in events, processes, and operations, whereas value objects do not and need not have uniqueness.
  2. Substance focuses on uniqueness and continuity, and does not care about the change of its attributes, which have all changed, and it remains what it was; A value object is descriptive and sensitive to changes in attributes, which means it is not what it is (meaning immutable, it may come from an external query).
  3. While the strategic thinking framework is stable, the tactical model design is flexible, and entities and value objects can move around depending on the business concerns of the system. For example, if you had a special bounded context that focused more on the address and less on the person associated with the address, you would design the address as an entity and the person as a value object.

3. The DDD

3.1 From three-tier model to DDD

Here is a brief introduction to a change from the three-tier model to DDD.

As you can see, the main thing is to split the service. Generally, it can be divided into three layers:

  • Application service layer: Encapsulates, orchestrates and combines multiple domain services or external application services to provide coarse-grained services externally. Application service is an independent business logic that mainly realizes service composition and orchestration.
  • Domain service layer: Composed of multiple entities, a method may be invoked across entities. When the code is too complex, each domain service can be split into a single domain service class, rather than putting all domain service code into a single domain service class.
  • Entity: is a hyperemic model. The logic associated with the same entity is implemented in the entity class code.

3.2 Introduction to Modeling

There are three steps we can take to define the boundaries between domain models and microservices.

  • Step 1: Sort out user operations, events and external dependencies in the business process in the event storm, and sort out domain objects such as domain entities according to these elements.
  • Step 2: Combine entities closely related to the business to form an aggregation based on the business relevance between domain entities, and identify aggregate roots, value objects, and entities in the aggregation. In the figure in Chapter 2, the boundary between the aggregations is a layer 1 boundary that runs in the same microservice instance. This boundary is a logical boundary, so it is represented by a dotted line.
  • Step 3: The domain model is formed by delineating one or more aggregations within a bounded context based on business and semantic boundaries. In the figure of Chapter 2, the boundary between bounded contexts is the boundary of the second layer, which may be the boundary of future microservices. The domain logic in different bounded contexts is isolated in different microservice instances and physically isolated from each other, so it is the physical boundary. Solid lines are used to represent the boundary.

3.3 Practice: Design a MiniStack

For your understanding, I’m going to design a very simple Iaas platform and plug in the most basic DDD concepts.

3.3.1 Product Vision

  • For: enterprise’s internal developers, operation and maintenance personnel
  • Their: computing, storage, network resource management
  • This: MiniStack
  • Is one: private cloud platform
  • It can: manage computing, storage, and network resources, and help users create VMS easily and quickly
  • Not like: OpenStack
  • Our products: simple, robust and intelligent

Strung together: In order to meet the needs of internal developers and operation and maintenance personnel of enterprises for their hardware resource management, we are constructing the MiniStack. It is a private cloud platform that can manage computing, storage and network resource management and help users create virtual machines easily and quickly. Unlike OpenStack, our products are simple, robust and elastic.

3.3.2 Scenario Analysis

For space reasons, let’s talk about the most typical scenario — creating virtual machines to work out the domain model.

It is important to note that we want to tease out as much as possible the actions, commands, domain times, and dependency changes that occur throughout the system.

3.3.2.1 Creating a VM

  1. User login system: verify information from the database to complete login authentication

  2. Create a VM: Enter the VM name, cluster, computing specifications, L3 network, and image. You can specify the physical machine and network segment, if necessary.

  3. The VM service must provide an interface for creating VMS

  4. Submit to the MiniStack engine to start the related scheduling:

    1. Search for low-load physical machines that match computing and storage resources, and update the physical machines to which VMS belong
    2. The dedicated server service needs to provide a query interface
    3. Allocate idle IP addresses in L3 network and update vm network information
    4. Network services need to provide IP assignment interfaces
    5. Tell the physical machine agent to pull the image from the image server to the physical machine found in step 1
    6. The physical machine service needs to provide an interface for pulling mirrors
    7. Tell the physical machine agent the startup parameters and pull up the VM
    8. The VM service needs to provide a startup interface
  5. The vm is created successfully, and the user can view the VM

But after creating the virtual machine is not so finished, in case of which day this physical machine carsh? On what day is the CPU full due to a strange process? So for our goal of intelligence, the MiniStac collects a series of monitoring information every 5 minutes after the VM is created:

  1. Send heartbeat packets to the dedicated server agent to ensure that the dedicated server is running properly
  2. The heartbeat packet is sent to the VM Agent, and the following information is returned: computing, storage, and network status

3.3.3 Macro design: Domain modeling

In this step, we need to analyze the business and build a domain model. The general steps are:

  1. Find domain objects such as domain entities and value objects
  2. Find the aggregate root and build the aggregate based on the dependencies of entities, value objects and the aggregate root
  3. The third step is to define the bounding context according to business and semantic boundaries

3.3.3.1 Defining entities

We can roughly identify several entities:

  • The virtual machine

    • Start the
    • stop
  • Storage resources of the dedicated server

    • The query
    • distribution
    • The release of
  • Computing resources of the dedicated server

    • The query
    • distribution
    • The release of
  • L3 network

    • Assign IP
  • Image server

    • The query image
    • Add a mirror
    • Release the mirror

3.3.3.2 Defining aggregation and bounding contexts

Before looking for an aggregate, we need to find the root of the aggregate. It can be divided into physical machines, networks, image servers, and virtual machines. They are independent contexts of each other. If necessary, they can also be divided into micro-services. If the application is a single application, it is recommended to use modules for logical isolation.

3.3.4 Micro: Domain object and code structure analysis

Once we have completed the macro modeling, we can start to do the micro things: tease out the domain objects within the microservices, tease out the relationships between domain objects, determine their location in the code model and the hierarchical architecture, establish the mapping between the domain model and the microservice model, and establish the dependencies between services.

Roughly speaking, there are two steps:

  1. Analyzing domain objects
  2. Design code structure

3.3.4.1 Analyzing domain objects

In this step, we need to confirm:

  • Layering of services
  • Which services comprise the application service
  • What entities and entity methods are included in the domain service
  • Which entity is the aggregate root
  • What attributes and methods an entity has
  • Which objects are value objects

Since our use case is relatively simple, it is reorganized as follows:

  • Application services:

    • VM creation services: Responsible for creating VMS and scheduling a number of underlying domain services
  • Domain services: VM service, physical machine service, network service, and image service

    • VM services: Manage the VM life cycle, such as creating, deleting, starting, and stopping VMS
    • Dedicated machine service: dedicated machine-related services, such as adding, deleting, status change, heartbeat awareness, and resource RUD
    • Network services: network related services, such as creating and deleting L2 and L3 networks, IP management, etc
    • Image service: services related to the image server, such as adding, deleting, status changing, and adding images
  • Entity: VM entity, physical machine entity, local storage entity (physical machine storage)

    • VM entities: start, stop, etc
    • Physical machine entity: status change, heartbeat awareness, etc
    • L3 entities: IP segment addition, deletion, IP allocation, release, etc
    • Local storage entity: Storage occupation and release

Looking at the objects in the aggregate, we identify several aggregate roots:

  • The aggregation root of a physical machine aggregation is a physical machine
  • The aggregation root in network aggregation is L2 network
  • The aggregation root in image aggregation is the image server
  • In vm aggregation, the root of the aggregation is the VM entity

The entity attributes and methods mentioned above are already represented in the diagram.

For the value object, see [ZStack] 7. Label system. The design is used in real production.

3.3.4.2 Design code structure

Once we had done the analysis of the domain objects, we began to design how the domain objects would be rendered in the code model — that is, mapping the domain objects to the code objects. Based on this mapping, the service staff can quickly locate the code location where the business logic resides.

Macroscopically, we can refer to the following hierarchical model:

For micro implementation, we can refer to COLA.

4. Summary

This article went through DDD with everyone and designed a project “out of thin air” in the article. In fact, this project is not out of the blue. I took my previous open source project ZStack and simplified it — the project is currently running in the private cloud of a large number of enterprise users and has been iterating for more than 6 years. Therefore, no matter from the design or landing, there are certain reference experience.

In order to make it easy for you to understand the examples in the article with the ZStack code, I made a map here.