preface

One of the biggest challenges of microservices is defining the boundaries of individual services. The general rule is that services should only do “one thing” (see the SRP principle) – however, implementing this rule requires careful consideration. No mechanical process can produce a “proper” design. Business domains, needs, and goals must be considered in depth. Otherwise, you can end up with a messy design that presents unwanted characteristics, such as hidden dependencies between services, tight coupling, or poorly designed interfaces.

In the paper [The first step in microservice architecture — service boundary demarcation], the domain-based microservice demarcation method is briefly introduced. Due to space limitations, I did not further analyze the construction of the domain model, and many students expressed a feeling of half-understanding. Borrow Lin Qun academician’s words: false pass ten thousand books, true pass a case. A good case can make people instantly understand the truth, rather than reading thousands of books. This article will use the official case of Microsoft Azure — drone delivery service, and introduce how to divide micro-services based on domain model in detail, hoping to be helpful to students.

DDD basics review

Domain-driven Design (DDD) believes that microservices should be designed around business functions as much as possible, rather than horizontal layers such as data access or messaging. In addition, microservices should be characterized by low coupling and high cohesion. If updating one service does not require updating other services at the same time, the microservice is low coupled. Microservices are highly cohesive if they have a single and well-defined responsibility, such as managing user accounts or tracking delivery history. Services should encapsulate domain knowledge, making it abstract to the client. For example, clients should be able to schedule drones without knowing the dispatching algorithm or how to manage a swarm of drones.

DDD provides a framework that allows you to smoothly design a comprehensive set of microservices. DDD consists of two distinct phases: strategic and tactical. In the strategic pattern of DDD, the large-scale structure of the system can be defined. Strategic patterns help ensure that the architecture is focused on business functions. Tactical DDD provides a set of design patterns that can be used to create domain models. These patterns include entity, aggregation, and domain services. Using these tactical patterns, you can design microservices with low coupling and high cohesion.

In this article and the following steps, we will step through the following steps and apply them to the drone delivery application:

  1. We first analyze the business domain to understand the functional requirements of the application. This step outputs an informal description of the domain, which can be optimized into a more formal set of domain models.
  2. Next, the Bounded Context of the domain is defined. Each bounded context contains a domain model that represents a specific subdomain of a large system.
  3. In a bounded context, the tactical DDD pattern is applied to define entity, aggregation, and domain services.
  4. Use the results of the previous step to identify the microservices in the application.

Keep in mind that DDD is an iterative, ongoing process. Service boundaries are not static. As your application evolves, you can decide to break up a service into smaller services.

ℹ️ This article does not provide a comprehensive domain analysis. We purposely kept short notes to illustrate the main points. For more background on DDD, we recommend reading Eric Evans’ Domain-Driven Design book, which first introduced the term. Another excellent resource is Implementing Domain-Driven Design by Vaughn Vernon. You can also see my domain Driven Design column.

In the strategic phase of DDD, we map the business domain and define the bounded context of the domain model. In the tactical DDD phase, the domain model needs to be defined more precisely. Tactical patterns are applied in a single bounded context. In microservices architecture, we are particularly interested in entities and aggregation patterns. Applying these patterns helps identify the natural boundaries of services in your application. As a general rule, a microservice should be no smaller than an aggregation and no larger than a bounded context. First, let’s look at tactical patterns. We then apply these patterns to the “delivery” bound context in the drone Delivery application.

Tactical Model Overview

This section provides a brief overview of the tactical DDD pattern, which you may want to skip if you are already familiar with DDD. These patterns are described in more detail in Chapters 5-6 of Eric Evans’ book, and in Implementing Domain-Driven Design by Vaughn Vernon.

Entity. Entities are objects that are always uniquely identified. For example, in a banking application, customers and accounts are entities.

  • An entity has a unique identifier in the system that can be used to find and retrieve the entity. This does not mean that the identifier is always directly exposed to the user. It could be a GUID or primary key in the database.
  • An identity can span multiple bounded contexts and may be retained beyond the end of the application life. For example, bank account numbers or government-issued ID numbers are not associated with the lifetime of a particular application.
  • Attributes of entities can change at any time. For example, a person’s name or address may change, but he or she remains the same person.
  • An entity can contain references to other entities.

Value object. The value object has no identity. It is defined only by its attribute values. Value objects are also immutable. To update a value object, you always need to create a new instance to replace the old one. Value objects can contain methods that encapsulate domain logic, but these methods should not negatively affect the state of the object. Typical examples of value objects include color, date time, and currency values.

Aggregation. Aggregation defines conformance boundaries for one or more entities. An aggregate contains only one root entity. You can use the identifier of the root entity to perform the lookup. A reference starting at the root can find any other entity in the aggregation.

The purpose of aggregation is to model transaction consistency. Things in the real world have complex relationships. Customers create orders, orders contain products, products have suppliers, and so on. If an application modifies multiple related objects, how does it ensure consistency?

Traditional applications typically use database transactions to enforce consistency. However, this is usually not feasible in distributed applications. A single business transaction may span multiple data stores, run for a long time, or involve third-party services. Ultimately, it is the application, not the data layer, that enforces the consistency required by the domain. This is the purpose of modeling for aggregation.

ℹ️ aggregation can contain a single entity and no child entities. Aggregation is defined by transaction boundaries.

Domain services and application services. In DDD terms, a service is an object that implements some logic and does not hold any state. Evans distinguishes between domain services, which encapsulate domain logic, and application services, which provide technical functions such as user authentication or sending SMS messages. Domain services are typically used to model behavior across multiple entities.

ℹ️ Software development uses the term “service” extensively. The definition here is not directly relevant to microservices.

Domain events. You can use domain events to notify other parts of the system when something happens. As the name implies, a domain event should represent something that happens in a domain. For example, “a record was inserted into a table” is not a domain event. “Cancelled delivery” is a domain event. Domain events are closely related to microservices architecture. Because microservices are distributed and do not share data stores, domain events can provide a way for microservices to coordinate with each other.

The article interservice communication discusses asynchronous messaging in more detail.

There are several other DDD patterns not listed here, including factory, Repository, and module. These patterns can be useful when developing microservices; However, they are of little use when designing boundaries between microservices.

From domain models to microservices

What is the appropriate size for microservices? We often hear people say, “Not too big, not too small” — which is absolutely true, but actually doesn’t mean much. However, it is much easier to plan out microservices if you start with a well-designed domain model.

With a set of entities, aggregates, and domain services identified for the bounded context, we can move from the domain model to the application design. Here is a method to derive microservices from the domain model.

  1. Start by qualifying the context. In general, functionality in microservices should not span multiple qualified contexts. By definition, a bounding context marks the boundaries of a domain-specific model. If you find that microservices mix different domain models, it may mean that domain analysis needs to be redone to optimize the domain model.

  2. Next, look at the aggregation in the domain model. Aggregation is often a good candidate for microservices. Well-designed aggregation can embody many of the characteristics of a well-designed microservice, such as:

    • Aggregation is derived from business requirements rather than technical factors such as data access or messaging.
    • The polymerization should have high functional cohesion.
    • Aggregation is the boundary of persistence.
    • The aggregation should be loosely coupled.
  3. Domain services are also an appropriate candidate for microservices. Domain services are stateless operations that span multiple aggregations. A typical example is a workflow involving multiple microservices. We’ll see an example of this in the drone Delivery application.

  4. Finally, consider non-functional requirements. Analyze factors such as team size, data types, technology, scalability, availability, and security requirements. These factors may lead to the need to further decompose microservices into two or more smaller services or, conversely, consolidate multiple microservices into one.

After you have identified microservices in your application, validate your design against the following criteria:

  • Each service has a single responsibility.
  • There are no trivial calls between services. If splitting the functionality into two services results in excessive trivialization, the cause of this symptom may be that the functionality belongs to the same service.
  • Each service is small enough to be built by small teams working independently.
  • The deployment of two or more services should not be interdependent. It should always be possible to deploy a service without redeploying any other services.
  • Services are not tightly coupled and can evolve independently.
  • Service boundaries do not create data consistency or integrity issues. Sometimes, data consistency must be maintained by putting functionality into a single microservice. Having said that, is there really a need for strong consistency? There are strategies to address final consistency in distributed systems, and the benefits of decomposing services often outweigh the challenges of managing final consistency.

Above all, be pragmatic and remember that domain-driven design is an iterative process. When in doubt, start with coarse-grained microservices. It is easier to break an existing microservice into smaller services than to refactor functionality across multiple existing microservices.

Actual case: drone delivery

Fabrikam, Inc. is launching a drone delivery service. The company operates a fleet of drones. Businesses sign up for the service, and users can request the drone to pick up items to be delivered. When the user arranges to pick up an item, the back-end system assigns a drone and informs the user of the estimated delivery time. During delivery, users can track the location of the DRONE using an updated ETA (Estimated Time of Arrival).

This scheme covers a fairly complex area. Some of the business challenges include scheduling drones, tracking packages, managing user accounts, and storing and analyzing historical data. In addition, Fabrikam hopes to launch quickly to expand the business while adding new features. The application needs to run in a cloud environment with a high service level objective (SLO) attached. In addition, Fabrikam anticipates that different parts of the system will have very different requirements for data storage and queries. All of these considerations led Fabrikam to choose a microservices architecture for drone delivery applications.

Analyze Domain

Designing microservices with the DDD approach enables each service to meet business functional requirements. This approach helps to avoid organizational boundaries or technology choices dominating your design.

Before writing any code, you need to get a bird’s eye view of the system you are creating. DDD starts by building the business domain and creating a domain model. A domain model is an abstract model of a business domain. It extracts and organizes domain knowledge and provides a common language for developers and domain experts.

First, map all business functions and the connections between them. This may require collaboration between domain experts, software architects, and other stakeholders. There is no need to use any particular form. Diagrams can be sketched or drawn on a whiteboard.

When you draw a diagram, you can begin to identify discrete subdomains. What functions are closely related? What functions are core to the business? Which functions provide ancillary services? What is a dependency diagram? At this initial stage, no technical or implementation details need to be considered. That is, you should pay attention to where your application integrates with external systems such as CRM, payment processing, or billing.

After completing some initial domain analysis, Fabrikam’s team drew up a sketch depicting the drone delivery space.

  • Shipping is at the center of the diagram because it is the core of the business. Any other elements in the diagram are intended to support this functionality.
  • Drone Management is also at the heart of the business. Closely related to drone management include drone maintenance and the use of predictive analytics to predict when a drone will need overhaul and maintenance.
  • ETA analysis provides estimated pickup and delivery times.
  • If the package cannot be delivered entirely by drone, the app can arrange alternative delivery via third-party shipping.
  • Drone sharing is a possible extension of the core business. Companies may have a surplus of drones at certain times, in which case they can be rented out to avoid idling. This feature was not included in the initial release.
  • Video surveillance is another area that companies can expand into in the future.
  • User accounts, Invoicing, and call centers are subdomains that support the core business.

Please note that no implementation or technology decisions have been made at this stage. Some subsystems may involve external software systems or third-party services. Even so, applications need to interact with these systems and services, so they must be included in the domain model.

ℹ️ If the application relies on external systems, there is a risk that the data architecture or apis of the external system will infiltrate the application and ultimately expose the architectural design. This is especially true for legacy systems that do not follow best practices and use complex data architectures or outdated apis. In this case, boundaries must be well defined between these external systems and the application. For this purpose, consider using strangler mode or preservative layer mode.

Define a Bounded Context

The domain model will contain representations of things in the real world — users, drones, packages, and so on. But that doesn’t mean that every part of the system needs to use the same representation for the same thing.

For example, a subsystem dealing with uav maintenance and predictive analysis would need to represent many physical characteristics of the UAV, such as its maintenance history, mileage, year of production, model, performance characteristics, and so on. However, we do not need to care about these aspects when arranging delivery. The planning subsystem only needs to know if the drone is available and the ETA for pickup and delivery.

If you try to create a single model for the two subsystems, you add unnecessary complexity. In addition, the model can be more difficult to evolve because any changes need to satisfy the requirements of multiple teams working on different subsystems. Therefore, it is often better to design different models that represent the same real entity (in this case, the drone) in two different contexts. Each model contains only the functions and attributes relevant to its particular context.

This is where the concept of DDD bounded context comes into play. A bounded context is simply a boundary in a domain to which a domain-specific model is applied. In the figure below, we can group functions based on whether they share a single domain model.

Bounded contexts are not necessarily independent of each other. In this figure, the solid line connecting the bounding context represents where the two bounding contexts interact. For example, “Delivery” relies on “user accounts” to get information about customers and “drone management” to arrange drones in a fleet.

In Domain Driven Design, Eric Evans describes multiple patterns that keep a Domain model intact when it interacts with another bounded context. One of the main tenets of microservices is that services communicate through well-defined apis. This approach corresponds to two patterns, what Evans calls “Open Host Service” and “Published Language.” The idea of “open hosting services” is that a subsystem defines a formal protocol (API) for the other subsystems it communicates with. The Release Language extends this thinking by publishing the API in a specific format that other teams can use directly to write clients. For example, use the OpenAPI specification (formerly known as Swagger) to define language-neutral interface specifications (expressed in JSON or YAML format) for REST apis.

The following sections focus on the shipping bound context.

Define Entities, Aggregates and Services

First, we explore the scenarios that the shipping bound context must handle.

  • A customer could request a drone to pick up a package from a company registered in the system.
  • The sender generates a tag (bar code or RFID) and attaches it to the package.
  • The drone will pick up the package and deliver it from its starting location to its target location.
  • When a customer schedules a delivery, the system will provide an ETA based on route information, weather conditions and historical data.
  • When the drone takes off, users can track their current location and the latest ETA.
  • Customers can cancel delivery before the drone picks up the package.
  • The customer will be notified when the delivery is completed.
  • Senders can request confirmation of receipt in the form of signatures or fingerprints.
  • Users can find a history of completed deliveries.

In these scenarios, the development team identified the following entities.

  • The delivery
  • The parcel
  • drone
  • account
  • confirm
  • notice
  • tag

The first four items (” delivery, “” package,” “drone,” and “account”) are aggregates that represent transactional consistency boundaries. “Acknowledge” and “notice” are the child entities of “deliver,” and “mark” is the child entities of “parcel.”

The value objects in this design include LOCATION, ETA, PackageWeight, and PackageSize.

For demonstration purposes, a UML diagram of the “drop” aggregation is provided below. Note that this aggregation contains references to other aggregations, including “account,” “package,” and “drone.”

There are two domain events:

  • When the drone takes off, the “drone” entity will send the DroneStatus event, which describes the position and status of the drone (in-flight, landed).
  • Whenever there is a change in the delivery phase, the “delivery” entity will send the DeliveryTracking event. These events include DeliveryCreated, DeliveryRescheduled uled, DeliveryHeadedToDropoff and DeliveryCompleted.

Note that these events describe what makes sense in the domain model. They describe some information about a domain, but are not related to a specific programming language construct.

The development team also identified another functional area that is not closely related to any of the entities described above. Some part of the system must coordinate all the steps involved in scheduling or updating the delivery. Therefore, the development team added two domain services to the design: a scheduler that coordinates steps, and a supervisor that monitors the status of each step to detect if any steps fail or time out. This is a variation of the Scheduler Agent Supervisor pattern. The diagram of the modified domain model is as follows:

Identify Microservices

Four aggregations (” delivery “, “parcel”, “drone” and “account”) and two domain services (” scheduler “and” monitor “) have been identified earlier.

“Delivery” and “parcel” are the top candidates for microservices. The Scheduler and monitor programs coordinate the activities performed by other microservices, so it is advantageous to consider these domain services as microservices.

“Drone” and “account” are special in that they belong to other bounded contexts. One way to do this is to have the scheduler call the drone and account bounded contexts directly. Another approach is to create “drone” and “account” microservices within the context of “delivery” limits. These microservices act as intermediaries between bounded contexts by exposing apis or data architectures better suited to “shipping” contexts.

The details of the Drone and Account bounded contexts are beyond the scope of this article, so we created mock services for them in the reference implementation. But in this case, there are some factors to consider:

  • How much network overhead does it cost to call directly into other bounded contexts?
  • Is the data architecture of other bounded contexts suitable for this context, or is it better to customize one specifically for this bounded context?
  • Are the other bounded contexts old-style? If so, you can create a service that acts as an anticorrosion layer to transition between legacy systems and newer applications.
  • What is the team structure? Is it easy to communicate with the team responsible for other bounded contexts? If not, creating a service that acts as an intermediary between the two contexts may help reduce the cost of cross-team communication.

So far, we have not considered any non-functional requirements. Given the throughput requirements of the application, the development team decided to create a separate “access” microservice responsible for accessing client requests. This microservice implements load balancing by putting incoming requests into a buffer for processing. The scheduler reads the request from the buffer and executes the workflow.

Non-functional requirements force the team to create an additional service. So far, all services are related to the real-time scheduling and delivery of packages. However, the system also needs to store the history of each delivery in long-term storage for data analysis. The team felt it was the delivery service’s responsibility. However, historical analysis is quite different from the data storage requirements of current operations. Therefore, the team decided to create a separate delivery history service that would listen for DeliveryTracking events from the delivery service and write them to long-term storage.

Here the division of microservices is basically completed. The following figure shows the final microservice design:

reference

  • Domain-Driven DesignEric Evans (Domain Driven design)
  • Domain Modeling for Microservices
  • Strangler model
  • Coating pattern