Strategic Domain-Driven Design
UPD: Article#2. Big picture Event storming
Microservice-based architecture became a standard for designing a modular scalable software architecture. At a certain moment of growth, many companies face challenges of splitting monolithic applications into microservices. One of the most complicated steps in such challenges is how to define the proper responsibilities of each microservice, which will not allow them to grow into a bunch of smaller monoliths. And how to communicate microservices while keeping a high level of maintainability and evolvability.
In a set of articles about using DDD for microservices design, I’d like to share my experience adopting Domain-Driven Design in tech companies and integrating it into a software development process.
These articles could help you to use DDD not only for decoupling monoliths or designing microservices-based architecture but also to better structure and communicate engineering teams.
Intro
Domain-Driven Design is a philosophy, methodology, and a set of patterns, used for designing software for complex business domains.
When starting developing software, many think about the data structures first to design the DB tables and connected classes and modules.
But DDD offers to reflect knowledge about your business domain to software design by using a set of techniques and design patterns.
As an example, let’s discover a simplified Uber-like hailing application.
Passenger makes a Booking. And Driver monitors and accepts a Booking from a Bookings list.
How could the application be designed based on data structures?
At first glance, we have the same Booking data structure used in two applications, Passenger & Driver. Location, payment method, and price of a Booking are presented to both parties. So, the whole solution could be designed as a set of connected tables with the Bookings table in the core.
Then, we add appropriate programming code units: modules, classes, and interfaces.
Since the data to be presented to both types of users has much in common, the code could partially have generic interfaces and a shared behavior. Thus we’ll design the hailing app as a single, all-encompassing model.
It could be dangerous because while the complexity grows with the number of entities and their relationships, each specific concept of Passenger and Driver will require functionality with more specific responsibilities.
E.g. for a passenger the Booking could have attributes such as Pickup Address and Price. The Pickup Address is the passenger’s current point of waiting and the start of the physical ride. Price includes all the fees to the application owners and the Driver and is processed as a money transfer from the Passenger’s account to the application’s account.
But for Driver, a Pickup Address is just an intermediate point, while the Ride starting point is the one where he accepted Booking. Price, shown to Passenger is not presented to Driver, but some margin is.
We can see that data of a single Booking needs lots of preprocessing before being presented to different parties and has different meanings depending on a context.
Without careful design, the system has chances to grow into a Big Ball of Mud — a monolithic application consisting of tightly coupled modules with lowly cohessed elements.
The maintenance complexity of such monoliths increases exponentially with time. Often we deal with legacy parts that are just working, but the knowledge about the logic of the feature is lost with time or gone with specific engineers who left the team already.
In contrast to a data schema-first approach, let’s try to reflect the business domain into the software systems with Domain-Driven Design patterns. DDD is tackling the complexity of the business domain by aligning domain experts on the problem space and engineers who will implement the solution space.
In its core, DDD consists of Strategic and Tactical design.
Strategic Design is a set of principles and patterns for maintaining model integrity, distilling the Business Domain Model, and working with multiple models. Strategic design is very useful to divide a large and complex business problem into multiple chunks with clear boundaries and specific responsibilities and build a high-level software design topology.
Tactical Design is a set of design patterns and building blocks that you can use to abstract low and mid-level components of the software. Tactical patterns are very hands-on addressing programming code and deal with classes and modules. The purpose of tactical design is to refine the result of Strategic patterns applied to a stage where it can be converted into a working code.
Domain-Driven Design is an evolving process consisting of iterative cycles of applying strategic and tactical design. You start with strategic design, followed by tactical design.
This article is mostly focused on what the problem space is and how its growth is addressed with DDD Strategic design.
Strategic design
Business Domain is the problem we are trying to solve with a software effort. So, the domain is the problem space, or what is addressed.
A domain can be split into Subdomains, e.g. Booking, Accounting, Identity Management.
Domain model — is an abstraction of the domain, which incorporates the behavior and data necessary to satisfy business requirements. So, the Domain model is a solution space, or how we’re going to solve the business problem
The domain model is consisting of Bounded Contexts — segments of the solution.
Bounded Context is a semantic contextual boundary. This means that within the boundary each component of the software model has a specific meaning and does specific things. The components inside a Bounded Context are context-specific and semantically motivated.
Bounded Context is a semantic contextual boundary. This means that within the boundary each component of the software model has a specific meaning and does specific things.
©Vaughn Vernon. Domain-Driven Design Distilled.
Designing a Problem Space
At first, to structure the business domain, we split it into subdomains to modularize the problem space. Splitting a business domain into subdomains is not trivial. The main challenges with splitting relate to handling the boundaries: how to identify the boundaries of each context, their overlapping areas, and how to deal with those areas.
In the problem space of our hailing app example on a high level, we have two subdomains: Passengers application and Drivers application.
In the Passenger subdomain, a customer creates a booking request. Customers can use Vouchers and manage Payment methods.
In the Driver subdomain, a licensed Driver accepts a Booking from the list and manages the appropriate physical ride.
Pic 1. Hailing app problem space
In order to design a solution space, we need to match each subdomain to an appropriate Bounded Context.
Designing a Solution Space
In DDD, a Subdomain in the problem space is ideally mapped 1-to-1 to a Bounded Context in the solution space.
Let’s at first solve the most complex part, the overlapping of subdomains. The first option to solve the overlapping is a Shared Kernel pattern. Shared Kernel is used when several bounded contexts can share one or many common entities. Using Shared Kernel in our case would mean a direct matching of subdomains and concepts from the Pic1 into Bounded Contexts and Entities with shared Booking and Payment.
The second option is a distinct Bounded Context holding specific entities. From the perspective of microservices architecture, the second option is usually more preferred and sustainable in the long-term, as any code unit tends toward a single responsibility principle.
On Pic 2 a hailing app problem space is abstracted with distinct Entities living inside their own split Bounded Contexts.
Pic 2. Hailing app solution space
With time the hailing business is growing. We’re upgrading different subdomains and concepts inside them. Existing concepts, like Booking and/or Bookings list, could also expand into a new Back-office subdomain. Even the whole product could extend to a B2B market. As a result, the problem space is becoming complex and requires further iterations of investigation and matching to bounded contexts. So, with further iterations of business evolution, we need to loop over the problem space to find new subdomains and apply Strategic DDD again.
How to detect new subdomains?
If a team starts using a new term to express an existing concept or an existing term is used to express a new meaning, then it is a red flag for the team that the subdomain’s boundaries are overlapping.
The growing Business Domain is verbally expressed and shared with expanding vocabulary of the domain-specific terms and called Ubiquitous Language. UL is documented and spoken by all the collaborators of a tackled domain involved in the software development process. Rewording, it is called Ubiquitous because it is used ubiquitously by the engineers and the domain experts.
The UL is what defines Bounded Contexts, so the evolving language is shaping the business domain model.
Let’s speak about the hailing more in details and use the Booking concept as a meaningful example:
- Passenger makes a Booking with the attributes: Passenger Name, Pickup location, Drop Off location, Car class, Price, Payment Method, etc.
- The Booking request triggers the creation of an Offer which is added to the Offers list monitored by Drivers and Back-office. Offer has Price for Drivers, Amount of Passengers, Special requests, Passenger personal details, like Phone, Name, and Rate.
- Driver accepts an Offer and starts a physical Ride to pick up the Passenger and manage the full A-to-B service. The Ride has Arrival time, Start time, Length, Final price, and many others.
Reflecting on the evolving behavior and attributes, the Booking concept is starting to have different meanings in Passenger, Driver, and newly appeared Back-office subdomains. So, the Booking is naturally transforming and could be split into Booking, Ride, and Offer.
The next-iteration problem space looks like in Pic 3.
Pic. 3. Evolution of Hailing app domain
Growing further, the whole business can expand to the B2B market which will cause new meanings of existing concepts and evolution of subdomains. On each iteration of the domain discovery, it’s crucial to make the UL reacher, so the new meanings of concepts and new subdomains could be detected and reflected in future system design.
Concepts and subdomains of the evolved problem space are then again matched to entities and bounded contexts in the solution space.
Let’s sum up the learnings
Ubiquitous Language and Bounded Context are strategic patterns in DDD.
The Strategic patterns are used to design large Business Domain Models described By Ubiquitous Language spoken inside their specific Bounded Contexts.
In short, DDD is primarily about modeling a Ubiquitous Language in an explicitly defined Bounded Context
©Vaughn Vernon. Domain-Driven Design Distilled.
As you see, the DDD approach requires business domain experts and software engineers to closely align on problem discovery and solution design. This pays off with a clear understanding of a business domain and clear and specific responsibilities of future software components.
Summary
DDD strategic patterns are used to design abstractions of Business domain models incorporating behavior and data. By collecting and using Ubiquitous Language we can detect a change in current and birth of new business subdomains. By structuring the Bounded Contexts and their relationships we can build a high-level topology of modular software design. The modular design helps to build microservices with clear responsibilities and thus increase the maintainability and evolvability of the system.
If that said, more questions are coming into the mind.
In the next article, we’ll talk about Event Storming workshop, which helps to facilitate the collaboration between domain experts and engineers and build UL.
Further, using the outcome of Event Storming, we’ll build a Context Map which helps to exactly define responsibilities and communications between microservices.