Domain-Driven Design — Designing Software in a Complex Domain
Modeling Software to align with business reality
The core function of the software is to address and resolve business challenges within an actionable context. In today’s enterprise environment, business domains are often vast, intricate, and constantly evolving, involving multiple stakeholders' collaboration. The software system demands adaptability, scalability, and maintainability to meet the solution of business problems & changing needs of the business. As a result, the software must be capable of addressing and aligning the intricacies of the domain.
Domain-driven Design (DDD) is an approach to software development that ensures the architecture is in complete harmony with the core business concepts and goals. By centering the design of the software system around the business domain, DDD ensures that the solution stays aligned with the actual business needs, which is crucial for consistently delivering value.
Domain-Driven Design was coined by Eric Evans in his book Domain-Driven Design: Tackling Complexity in the Heart of Software. It consists of patterns, principles, and collections that enable the team to focus on what is core to the success of the business.
“Domain-driven design (DDD) is an approach to developing software for complex needs by deeply connecting the implementation to an evolving model of the core business concepts” — dddcommunity.org
Domain & Subdomain
In DDD, the domain is the main area of business, and the software is built to model and solve problems. It includes all the knowledge, activities, and rules that define how the business works. The domain concept is very broad and abstract. To make it more concrete and tangible, splitting it into smaller parts called subdomains makes sense.
To make this large concept easier to handle, the domain is broken down into smaller sections called subdomains. These subdomains represent different parts of the business. For example, in an e-commerce application, the subdomains might include managing products, handling customer orders, processing payments, and organizing shipping. Each of these subdomains has its own specific rules and processes that need to be accurately represented in the software to ensure it reflects real-world business operations.
DDD Approach: Strategic & Tactical
Strategic DDD focuses on high-level architectural decisions that shape the overall structure of the software system. It involves identifying and modeling the core business processes, breaking the domain into subdomains, and defining the relationships and integrations between these subdomains. Key concepts like Ubiquitous Language, Bounded Contexts, and Context Maps play a vital role in providing a clear, strategic view of the domain.
On the other hand, Tactical DDD deals with the solution space, delving into the specifics of how the domain is implemented at the code level, particularly within the defined sub-domains (Bounded Contexts). It introduces various design patterns that enable developers to model the domain with precision, ensuring that every part of the system accurately follows the relevant business rules and processes.
Strategic DDD
1. Ubiquitous Language — Distilling the domain knowledge
Collaboration with domain experts and stakeholders is essential to accurately model a domain in software. This involves discussing, describing, and understanding the domain while building a shared language among the team, developers, and participants. The domain concepts must be expressed using the same terminology, meaning, and context, eliminating any ambiguity. The ultimate goal is to align the language used in conversations and ensure that this shared language is reflected consistently in the model and the code.
The Ubiquitous Language is modeled within a Subdomain (Bounded context) and there should be no ambiguity. E.g., in the Order Management Context, Order refers to a customer’s purchase, including items, payment, and billing details. In the Warehouse Management Context, Order Refers to a package that needs to be fulfilled, focusing on inventory and shipment. In the shipping management context, the order will focus on packaging and delivery processes, and delivery address.
It is context-specific, evolving, and must be reflected in the domain model. It unites team members, removes ambiguities, and is not imposed by domain experts or industries. Instead, it’s a carefully crafted agreement among all stakeholders to accurately model the domain and reject irrelevant concepts or jargon.
How to develop Ubiquitous Language?
To develop a Ubiquitous Language, start by involving domain experts and stakeholders in open discussions, as even they may disagree on terminology. Begin with visual representations on a whiteboard to map out the domain, even informally. Each subdomain may require a unique “dialect” of the language, reflecting specific business needs.
Next, create a glossary of key terms, ensuring consistency. Use “Event Storming” sessions to quickly capture business processes and foster collaboration between developers and domain experts. Regularly review and update the language to keep it relevant and agile. Ultimately, remember that the code is the lasting representation of the Ubiquitous Language, so it must accurately express the terms and concepts, making other documentation secondary. E.g. If you need to define a method to create a user during a subscription, use the method name “signupForSubscription” instead of simply naming it “create”.
Effective domain modelers are good knowledge crunchers. Refer here for insights into the development and challenges of the ubiquitous language.
2. Bounded Context — Large & Complex Domain? Divide to Conquer
“Wherever something is wrong, something is too big” — Leopold Kohr
Subdomains represent distinct business areas within an organization. They break down the business into smaller, logical segments to manage specific functions or processes. These subdomains are business-driven and represent major functional areas of the organization. For example, in an e-commerce application, subdomains might include Order Management, Inventory Management, and Payment Processing — each representing a different aspect of the business. Sub-domains can be core, supporting, or generic.
From a design (or implementation) perspective, ideally within a subdomain, we define technical boundaries where specific models operate. These boundaries are known as Bounded Contexts. Each Bounded Context has its own rules and terminology, and and “dialect” of ubiquitous language is applied consistently within that boundary. In large systems, instead of using a single unified model (which would be overly complex), we break down the system into smaller, more independent models with clearly defined boundaries.
For instance, a single subdomain can encompass multiple Bounded Contexts. In the Order Management subdomain, you might have separate contexts for Order Creation (focused on placing an order) and Order Fulfillment (focused on shipping).
While one-to-one mapping between subdomains and Bounded Contexts is ideal, real-world software systems may differ. A subdomain may be large enough to require multiple Bounded Contexts, or it may be simple enough to be combined with another within a single boundary.
Consider two Bounded Contexts — Payment and Shipping. In the Payment context, the customer is represented by attributes like credit card information, tax rate, business number, and billing address. Meanwhile, in the Shipping context, customer data includes details like delivery address and preferred delivery times. The domain experts involved in these two contexts may differ.
Identifying the Bounded Context in your domain
A deep understanding of domain knowledge is crucial for identifying bounded contexts. Clues like business relevance, autonomy, linguistic boundaries, data flow, and relationships with other contexts help reveal these boundaries. Begin by thoroughly analyzing the domain to build a strong foundation of insight.
Event Storming is a workshop-style collaborative method that brings together relevant stakeholders to explore the domain and identify business processes. In this approach, the sequence of domain events along with the action that triggered the domain events are identified, visually laid out — often using color-coded sticky notes, and mapped to create a clear visual view. From there, the clusters (aggregates) are formed, actors defined and any policies or external systems that influence the domain are identified to delineate the bounded contexts.
3. Context Mapping
Once bounded contexts have been identified, the next step is to map the relationships between them. Context mapping involves identifying, classifying, and visually representing these connections. Various context mapping patterns help illustrate these relationships, as shown in the diagram below.
Tactical Design Pattern — shaping code inside a Bounded Context
After establishing the conceptual model with strategic design, it’s time to dive into implementation specifics within a bounded context. Tactical design focuses on solving specific design challenges to create a robust codebase. Key building blocks for effective domain modeling in this phase include entities, value objects, and aggregates.
Entity
Entity represents a business concept as an object with a unique, immutable identity that persists over time. It has attributes (properties) and behaviors that may evolve, but its identity remains constant. Each entity has its lifecycle within the domain model; for example, a customer in an e-commerce application serves as an entity with attributes like name, email, and address.
Value object
Value object represents the business concept without identity and is defined only by the values of their attributes. In an eCommerce application, consider the example of “Money” — say, 100 AUD
.
- While you could treat currency as a string, this approach allows any combination, e.g.
ZWQ
, that isn’t a valid currency. Currency is a subset of possible string values, so using a value object becomes more precise, and can implement validation during object initialization. - For the
Money
value object, you could structure it with an amount andCurrency
(previous value object). Business rules can be applied, such as preventing negative “amount”, and methods like “add” can add two “Money” with the same currency. - In one bounded context concept can be modeled as a value object and in another as an entity:
Aggregate
Aggregate is the encapsulation of entities and value objects that defines the transactional consistency boundary. This structure ensures that the aggregate is created, retrieved, and stored as a single, cohesive unit, maintaining a consistent state at all times.
Let’s dive into the value objects, entities, and methods that make up an Order aggregate in an e-commerce application. When a customer places an order, it can include multiple products, each represented as an order item within the order.
- OrderId: A unique identifier of the aggregate.
- OrderItem: Entity that represents each product within an order with a unique orderItemId, along with attributes such as productId, quantity, rate, and value objects like price, and subtotal. It also has methods like increaseQuantity and decreaseQuantity to adjust the item quantity as needed.
- TotalAmount and TotalDiscount: Money value object of the order
- ValidateOrder: Ensures there is at least one “OrderItem” in the order.
- CalculateTotalAmount: Calculates the total cost of the order.
The aggregate is owned by an entity called the aggregate root, whose ID is used to identify the aggregate itself and the aggregate can be referenced from outside through this root-only entity only.
The aggregate is owned by an entity known as the aggregate root, whose ID uniquely identifies the aggregate. Access to the aggregate from outside is only possible through this root entity.
Repository and Factory
Factories are used to create new aggregates by supplying entities and value objects. Repositories provide encapsulation of the logic to access the persisted stores.
Code Implementation of DDD
For practical examples and code implementations following Domain-Driven Design (DDD) principles, check out these repositories:
- Node clean architecture
- Node.js Boilerplate
- Anemic Domain Model vs DDD
- ASP.NET ABP Framework
- Spring Framework
Layered Architecture
In Domain-Driven Design, a layered architecture is generally employed to organize/separate the system into distinct layers. This structure enhances the separation of concerns, and scalability, and simplifies maintenance and system comprehension. The diagram below depicts these layers in DDD.
User Interface
This layer is where users interact with the system. It displays information and takes in user actions or commands.
Application Layer
This layer handles the user interactions and coordinates business process flow, but it doesn’t contain any actual business rules. It acts as a shield, keeping the core domain logic separate from the rest of the system.
Domain entities are created and subjected to update here. Tasks such as concurrency management, handling cross-cutting concerns, integrating third-party services, and tracking the status of ongoing tasks are handled here.
Domain Model Layer
The data and behavior of the domain are conceptually modeled in the domain model. The entity, value objects, aggregates, factories, and interface are defined here to express the domain rules and logic. The domain model is generally designed without any dependency on another layer, third party, or the persistence layer. Then the application layer orchestrates those decisions.
For a deeper breakdown of domain service and application service, refer here.
Infrastructure Layer
The data that is initially held in the domain entities persist in databases or other persistent stores.
When is DDD applicable?
DDD is more than a coding recipe. It’s about a strategic mindset, a collection of ideas and approaches helpful to the system that needs to understand business semantics (concepts and behavior). However, it's not an all-or-nothing deal. We can implement the ideas (strategic or tactical approach) depending on your project.
The first step is to understand the domain and the specific problem your application aims to address. If the domain is primarily a bunch of data structures, there’s no need to overcomplicate the design. Domain-driven design (DDD) may not be the best fit for a CRUD-centric or simple application, as it’s tailored for more complex business domains. DDD isn’t a “how to” coding recipe; although it provides certain tools, it’s ultimately up to you, to decide if they align with your app’s needs.
Vaughn Vernon evokes his DDD scorecard to determine if it is worth applying DDD. This repo provides a step-by-step guide for learning and practically applying Domain-Driven Design.
DDD and microservices
Microservices is an architectural pattern for designing applications through loosely coupled, independently deployable, small, and reusable services (micro-units).
By applying DDD’s strategic insights — such as using bounded contexts to establish clear boundaries between services — microservices can be designed to better reflect real business capabilities and requirements, not merely technical separations, which can ultimately provide the agility needed to meet domain-specific demands. A single bounded context typically maps to one microservice, but it may also have one-to-many relationships.
Uber implemented a Domain-Oriented Microservice Architecture (DOMA), which is based on DDD. The guidelines for designing DDD-oriented microservices are here.
Challenges in applying DDD
Some of the common challenges while applying DDD include the need of the involvement of domain experts at the outset, managing abstractions overhead, risks of overengineering, a steep learning curve, and justification of domain modeling.
References
- Domain-Driven Design: Tackling Complexity in the Heart of Software
- Implementing Domain-Driven Design — Vaughn Vernon
- Patterns, Principals, and Practises of Domain-Driven Design - Scott Millett with Nick Tune
- https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice
- https://github.com/ddd-crew/ddd-starter-modelling-process
- https://eng.uber.com/microservice-architecture/
- https://netflixtechblog.com/ready-for-changes-with-hexagonal-architecture-b315ec967749