🌐 中文 English
Introduction
In today’s increasingly complex software systems, we often encounter such pain points: business logic is fragmented by frequently changing product requirements, and the code structure gradually evolves into an unmaintainable “big mud pit.”
From the initial simple traditional monolithic architecture to today’s complex distributed and microservices architectures, the evolution of software architecture has gone through multiple stages. In an era of high concurrency and prevalent microservices, how can we elegantly define service boundaries and manage business complexity? DDD (Domain-Driven Design) has become our ultimate tool to break the deadlock.
The Evolution of Software Architecture
To better understand the value of DDD, let’s first look at the three typical stages of software architecture evolution:
- Traditional Monolithic Architecture: All application functions are integrated into a single application, with all modules and components running within the same process. Requests directly operate on the database, sometimes without even code layering. While easy for early development and deployment, as the business grows, modules for different functions become severely coupled, where a change in one affects the whole.
- MVC Layered Architecture: The application is divided into different layers (e.g., controller, service, mapper). Layers interact through interfaces, promoting modularity. However, in the face of complex business scenarios, the flexibility and scalability of MVC architecture are limited. As the business grows, the service layer becomes extremely bloated, and maintenance costs gradually increase.
- Microservices Architecture: The system is split into multiple small, independent services. Each service is developed, deployed, and maintained by an independent team, communicating through lightweight protocols (e.g., HTTP, RPC, or message queues). Services are independent and easy to scale horizontally.
🚨 The “Artistic” Challenge of Microservice Splitting
With the popularity of microservices, the industry faces a new challenge: How granular should service splitting be? How should boundaries be defined?
- Splitting Too Fine: Project complexity skyrockets, with significant increases in interface network call costs, distributed transaction handling, and service operation and maintenance costs.
- Splitting Too Coarse: Business boundaries become blurred, services remain highly coupled, and the advantages of microservices are lost.
DDD is a methodology that guides us in determining business boundaries based on domain models, thereby defining application boundaries, which ultimately translate into service boundaries and code boundaries.
Core Concepts of DDD Domain-Driven Design
DDD defines domain models through domain-driven design methods to determine business and application boundaries, ensuring consistency between the business model and the code model.
1. The Three Major Goals of DDD
- Implement Business Requirements Through Domain Models: Enable developers and domain experts to jointly understand business requirements, form a shared language, and build models.
- Improve System Flexibility and Maintainability: Reduce system coupling through reasonable bounded context division, allowing different modules to evolve independently.
- Support the Expression of Complex Business Logic: Through in-depth business modeling, complex business logic can be clearly and accurately reflected in the code.
Core Idea: Make the system more aligned with the business and make large systems easier to build and maintain independently.
2. The Two Major Construction Phases of DDD
DDD construction is a combination of Strategic Design and Tactical Design.
① Strategic Design (Business-Oriented: Defining Boundaries)
Starting from the business, establish domain models and unify bounded contexts. Design typically involves Event Storming:
- On-Site Recreation: Domain experts, architects, developers, testers, product managers, and project managers gather together, covering a whiteboard with colorful sticky notes to brainstorm.
- Core Discussion: What businesses does the system involve? Which business action triggers another? What are the inputs and outputs?
- Converge Boundaries: Sort out the relationships between domain objects, perform clustering to form aggregates, aggregate roots, and bounded contexts, and finally complete microservice splitting.
During event storming, product design methods are often combined, such as Use Case Analysis (describing system interactions with the outside), Scenario Analysis (exploring user usage in different environments), and User Journey Analysis (depicting a series of steps from start to finish for the user).
② Tactical Design (Technology-Oriented: Code Implementation)
Starting from technical implementation, map domain models to code models. This phase is responsible for code implementation, including the design and implementation of code logic for aggregates, aggregate roots, entities, value objects, etc.
Hardcore Analysis of DDD System Terminology
To avoid “talking past each other” in actual coding, we need to thoroughly understand DDD’s core terminology:
| Core Term | One-Sentence Definition | Core Characteristics & Code Mapping |
|---|---|---|
| Domain | The problem space and boundaries the business cares about. | Used to determine scope and boundaries. Can be further divided into core domain (core competitiveness), generic domain (e.g., payment), and supporting domain (e.g., gateway). |
| Bounded Context | The business semantic environment with a unified ubiquitous language. | Defines business boundaries. For example: called “product” in e-commerce semantics, called “goods” in transportation semantics. The context ensures no ambiguity in team cognition. |
| Entity | A business object with a unique identifier (ID). | State is mutable but ID remains unchanged. In code, a rich domain model is typically used (related business logic is written directly in the entity class). |
| Value Object | An object without a unique identifier, only describing characteristics. | State is immutable; modification requires complete replacement. Example: the “address” attribute of a user. |
| Aggregate | A whole composed of multiple entities and value objects. | A highly cohesive, loosely coupled organization. It is the basic unit for data modification and persistence and the smallest unit for microservice splitting. |
| Aggregate Root | The leader within an aggregate, the unified external interface. | The aggregate root is also an entity with an ID. External parties must not directly access other entities within the aggregate; they must cooperate through interfaces provided by the aggregate root. |
| Domain Service | Cross-object business behavior that cannot belong to a single entity. | Encapsulates core business rules. Example: “order payment” logic involving interactions between multiple entities like order, account, and payment. |
💡 Viewing the World from a Dynamic Perspective: Entities and value objects are not set in stone. For a computer host, a graphics card is a value object (if broken, replace it with a new one); but for a graphics card manufacturer, the graphics card is an entity with a factory serial number that needs to be tracked throughout.
DDD Architecture Design: Rich Domain Model VS Anemic Domain Model
When implementing tactical design, how to organize the data and behavior of domain objects is the watershed between traditional development and DDD development. This essentially reflects different understandings of “responsibility division between data and behavior” in domain objects, mainly divided into anemic domain model and rich domain model.
| Dimension | Anemic Domain Model | Rich Domain Model |
|---|---|---|
| Core Definition | Domain objects only contain data (attributes) and getters/setters, without business logic. | Domain objects contain not only data but also business logic that processes this data. |
| Object Role | Pure “data containers,” with only state and no behavior. | True “object-oriented,” with both state and behavior. |
| Logic Ownership | All business logic is stripped into separate service classes (e.g., Service layer). | Business logic belonging to the object itself is directly cohesive within the object. |
| Applicable Scenarios | Suitable for simple business (e.g., traditional CRUD systems). | Suitable for complex business, allowing business logic and data to be closely combined. |
IV. Evolution of DDD’s Classic Four-Layer Architecture
After understanding the rich domain model, how do we build a stage for these vibrant domain objects? This is DDD’s four-layer layered architecture model. By dividing the application into four layers with clear responsibilities, it promotes high cohesion, low coupling, and high maintainability of the code.
1. Hardcore Breakdown of DDD Four-Layer Architecture
① User Interface Layer
- Positioning: Also called the presentation layer or web layer, primarily responsible for interacting with external parties (users, APIs of other systems, front-end pages).
- Responsibilities: Receive user input and return system output. This layer absolutely does not contain any business logic. It is mainly responsible for parameter validation (JSR303), authentication, and forwarding user request data (DTO) to the application layer.
② Application Layer
- Positioning: The “diplomat” and “chief director” of the entire system, mainly used for coordination and orchestration.
- Responsibilities: The application layer itself does not contain any core business rules. It implements specific business use cases by calling entities/domain services in the domain layer and resources in the infrastructure layer (e.g., sending emails, MQ messages). Simultaneously, distributed transaction control (
@Transactional), security permission validation, and external remote RPC calls also reside in this layer.
③ Domain Layer
- Positioning: The absolute core and soul of the entire architecture, purely cohesive business logic.
- Responsibilities: Contains the core business rules, strategies, and state transitions of the application. The aggregate roots, entities, value objects, and domain services mentioned earlier all reside here. Its design is completely decoupled from underlying technologies (e.g., databases, caches), solely to accurately transform real business requirements into code.
④ Infrastructure Layer
- Positioning: Provides underlying technical support and common services to the upper layers.
- Responsibilities: Responsible for physical interaction with external systems (e.g., MySQL, Redis, RabbitMQ). Provides data persistence implementation, logging, email sending, and other utilities.
2. Strict Layering VS Relaxed Layering
In the calling relationships of layered architectures, the industry typically has two schools:
- Strict Layering Architecture: Each layer can only depend on its direct lower layer. That is:
User Interface Layer->Application Layer->Domain Layer->Infrastructure Layer. This structure is the most robust, with each layer being a natural sandbox, achieving the most thorough decoupling. - Relaxed Layering Architecture: Interaction between layers is more flexible. For example, the user interface layer or application layer can bypass the domain layer and directly call the infrastructure layer to read underlying report data. This approach is very efficient for rapid development and handling pure query (CQRS) scenarios, but as system complexity increases, it can easily lead to dependency chaos.
Byolio Pitfall Avoidance Guide (Dependency Inversion Principle DIP): Traditional development habits involve upper layers depending on lower layers, causing the domain layer to be heavily bound to the database. DDD’s infrastructure layer adopts dependency inversion design (Dependency Inversion Principle). The domain layer only defines interface specifications for persistence (e.g.,
OrderRepositoryinterface), while the infrastructure layer is responsible for writing the concrete implementation of this interface (e.g.,OrderRepositoryImpl). This way, the entire core domain layer becomes a pure Java module that does not depend on any specific framework. Even if MySQL is later replaced with MongoDB, only the implementation in the infrastructure layer needs to be rewritten, while the core business logic remains rock-solid.
3. Transformation from Traditional Three-Layer Architecture to DDD Four-Layer Architecture
For students familiar with SpringBoot development, the most accustomed architecture is the three-layer architecture (Controller-Service-Dao). The following diagram and code structure intuitively show how they map and evolve into the DDD four-layer architecture:
📐 Responsibility and Concept Mapping Comparison
| Traditional Three-Layer Architecture (MVC) | DDD Four-Layer Architecture | Core Transformation Logic |
|---|---|---|
| Controller Layer (Presentation Layer) | User Interface Layer | Responsibilities are basically aligned, responsible for HTTP request handling, DTO validation, and response assembly. |
| Service Layer (Business Logic Layer) | Application Layer + Domain Layer | Core splitting point! The traditional all-purpose Service is split in two: process orchestration and third-party integration are left to Application, while core business rules are precipitated into entities and domain services in Domain. |
| DAO / Repository Layer | Infrastructure Layer | Traditional Dao only writes SQL. DDD’s infrastructure layer not only manages the database (implementing Repository interfaces defined by Domain) but also takes over all technical details like Redis, MQ, etc. |
📁 Evolution to SpringBoot Package Structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Traditional Three-Layer MVC Architecture
com.byolio.project
├── controller // Various Controllers
├── service // Bloated business logic, procedural-oriented
│ └── impl
├── model // entity objects / vo objects / dto objects
└── mapper // MyBatis Mapper
---------------- Evolves to ----------------
// DDD Four-Layer Architecture (Monolithic Module Package Structure Example)
com.byolio.project
├── interfaces // 1. User Interface Layer
│ ├── assembler // DTO converters, responsible for converting request parameters to domain layer entities.
│ ├── controller // Controller layer
│ ├── vo // vo objects
│ └── dto // Data Transfer Objects, responsible for request parameter validation and response assembly.
|
├── application // 2. Application Layer (Very thin, only responsible for assembly and orchestration)
│ └── service // Cross-domain business logic
|
├── domain // 3. Domain Layer (Core, pure Java, independent of specific technology frameworks)
│ ├── order // Divide bounded contexts by aggregate
│ │ ├── entity // Rich domain model entities (Aggregate Root)
│ │ ├── valobject // Value Objects
│ │ ├── repository // Pure interface definitions! No implementations written here.
│ │ └── service // Domain Services
|
├── infrastructure // 4. Infrastructure Layer (Concrete technical implementations)
│ ├── config // Configuration classes
│ ├── common // Common components
│ ├── api // API interfaces
│ ├── mapper // Database mapper layer
│ ├── manager // Infrastructure common components
│ ├── utils // Utility classes
│ └── repository // Core! Implements Repository interfaces defined in the domain layer
On the Introduction of shared (Shared Domain): Many students, when first writing DDD, fall into a rigid mindset—since strong isolation is required, each aggregate must be completely independent. As a result, objects like Money (amount value object) and BaseEntity (base class containing common ID and timestamp fields) are rewritten three or four times in various packages like order, user, product. The shared package is born to break this “rigid isolation.” It stores underlying common domain concepts that the entire system highly agrees upon and that have no business ambiguity. However, we must be vigilant that shared does not become a new big mud pit! Only those objects without specific business peculiarities (e.g., generic exception definitions, purely generic value objects) can be placed in shared. If part of OrderEntity is also extracted into shared for other aggregates to directly depend on for convenience, it completely destroys the boundaries of bounded contexts, leading to total failure. Remember: It’s better to have moderate duplication than to blindly share business logic.
Disadvantages of DDD
Although DDD can perfectly handle complex business, it is by no means a silver bullet. In practical implementation, it also has undeniable pain points:
- Extremely Steep Learning Curve DDD introduces many abstract concepts (e.g., bounded context, aggregate root, rich domain model, value object). Team members need to completely shift away from the traditional mindset of “CRUD / database table-driven” development, and the cost of this mindset transformation is very high.
- Pain Point of “A Small Change Affects Everything” in Field Modifications In traditional “anemic model + database-driven” development, if a table structure needs to add a field, often only the underlying Entity and top-level DTO need to be modified, and the data can “pass through” all the way. However, under DDD’s strict layering and anti-corruption mechanisms, due to the introduction of extensive object isolation, merely adding a field to the underlying table structure may require synchronously modifying 4 or 5 object definitions and corresponding Mapper converters. This high modification cost can be very frustrating during the early stages of agile development with frequent changes.
- Easily Becomes Superficial, Hollowing Out the Domain Layer (Formalistic DDD) This is the most common “derailment” scenario when domestic teams implement DDD. Due to developers’ deeply ingrained habits of “procedural orientation” and “Service mania,” in actual coding, people often prioritize speed and write all business logic directly in the topmost Application Service (application service layer), while the lower-level Domain entities and domain services become useless “messengers” and empty shells. This kind of formalistic DDD, which has “a four-layer architecture on the outside but an anemic core on the inside,” not only fails to enjoy the benefits of high object cohesion but also unnecessarily increases the workload of several layers of code calls, becoming a veritable “architectural burden.”
FAQ
Why Do DDD Architectures Differ Across Companies?
This is the most common confusion for students who have just read technical blogs and then look at enterprise-level source code from different companies. The answer is simple: DDD is a design philosophy (Methodology), not a rigid code specification (Framework).
The specific code architecture that lands depends on the trade-offs and compromises in the following three core dimensions:
1. Team Compromise Between “Strict Layering” and “Engineering Efficiency”
- Strict Layering: In some ultra-large financial, payment, or core e-commerce teams, they strictly adhere to isolation principles. The UI layer and domain layer are even in different Maven Multi-Module structures. To prevent any contamination, they prefer to write a large number of
Converters, where a single field may pass through 4 or 5 conversions. - Engineering Efficiency: Many small and medium-sized teams or rapidly iterating businesses, for the sake of “engineering efficiency,” adopt package isolation within a single module. They may even allow the
interfaceslayer, in query (CQRS) scenarios, to bypass the domain layer and go directly to the underlyingmapperthrough relaxed layering. Because they know that if a field change requires 5 conversions daily, team development efficiency would be directly dragged down.
2. Different Lifecycle Stages of “Monolith” and “Microservices”
- Monolith Evolution Stage: If the system is still a large monolith, when dividing the package structure, a
sharedpackage is often set up. Because within the same process, sharing some basic assets can greatly reduce code duplication. - Microservices Mature Stage: If the team has already split various aggregates into physically isolated microservices, they will basically eliminate the large, all-encompassing
sharedpackage. Because microservices emphasize complete decoupling and zero dependency. Even if two microservices have 80% similarMoneyvalue objects, they would rather copy one in each service to gain the ability for independent deployment and evolution.
3. The Influence of Conway’s Law: Organizational Structure Determines Code Architecture
“Organizations which design systems … are constrained to produce designs which are copies of the communication structures of these organizations.” —— Conway’s Law
Each company’s business complexity, and even its team personnel structure, is different:
- Some companies have extremely complex and frequently changing business (e.g., complex supply chain systems), so they design the
domainlayer to be very thick and heavy, heavily guarded. - Some companies claim to implement DDD, but in reality, 90% of their business is statistical reports and CRUD operations. Their
domainlayer naturally becomes an empty shell, or the entire architecture leans more towards the traditional anemic model, just wearing a four-layer skin.
byolio on Important Architectural Views: When learning DDD, never just copy the code skeleton of an open-source project and think you’ve obtained the true teachings. What we need to learn is its core of “making technology serve the business.” In engineering implementation, “there is no best architecture, only the most suitable compromise for the current business stage and team situation.” Tailoring and customizing the package structure that belongs to your own team based on business pain points is the real domain-driven design.
Summary
DDD is not a specific framework but a design philosophy that brings technology back to the essence of business. Through strategic design, we can clarify complex business boundaries at a macro level; through tactical design, we can use tools like entities and aggregate roots to write more expressive and robust code at a micro level.
When facing long-term, cross-departmental collaborative, and complex business maintenance projects, DDD can endow the system with strong vitality and evolutionary capabilities.