Introduction to Domain-driven Design: Part 1 of 3
Note: Domain-driven Design (DDD) is a vast and complicated subject. There are many excellent books available, and it can only be mastered through practical, real-world implementation experience.
In my view, Data Engineers and Architects have much to learn from this software engineering discipline on how to build scalable, distributed systems. Too many data platforms are being built by Engineers, delivered as centralized monoliths, with domain knowledge stripped off. The availability of cloud-scale data warehouses and metadata-driven ingestion and processing have only aggravated this situation.
In three posts, I provide a high-level, 30,000 feet overview of DDD, illustrating it through the example of an online commerce domain.
DDD is a methodology for designing software, that was outlined by Eric Evans in the early 2000s. The core idea is that, in order to build better software, it is necessary to align software design with the business requirements of the domain and achieve clarity of communication between domain experts and software professionals. This will ultimately result in reduced failures in software projects.
The DDD methodology provides strategic and tactical design patterns that guide the practitioners achieve this objective.
At a strategic level, DDD helps understand the problem domain, break it down into smaller parts and identify the relationships between them. DDD’s tactical design patterns help design and implement solutions that closely fit the problem domain.
All this is very abstract, so let’s look at some examples. I’ll illustrate DDD using the example of an hypothetical online marketplace bringing together buyers and sellers (e.g. Etsy or Amazon).
Strategic Design Patterns
To understand a domain deeply, you need to do what DDD experts call as knowledge crunching. This involves software developers communicating with domain experts to develop knowledge of the domain. The strategic design patterns help us achieve this objective.
The major strategic design patterns are: domains and subdomains, ubiquitous language and bounded context. Let’s look at them one by one.
Domains and Subdomains
To see how it’s done, let’s identify the sub-domains, the bounded contexts and the ubiquitous language that comprise our sample domain (i.e. the online commerce marketplace).
The first step is to identify the sub-domains that comprise the domain. One way is to look at all the distinct activities that happens in the enterprise. The graphic below describes the subdomains for the online commerce marketplace domain.
Subdomains can be core, generic or supporting. The core subdomains provide an opportunity for differentiation.
For instance, a platform like Etsy differentiates itself by creating a marketplace for DIY crafts, selling custom-made products that are hard to find on Amazon or eBay, sold in a neighborhood store styled setting, with terms friendly to small sellers, etc.
The generic subdomains tackle complex but well understood problems, which are done the same way across different enterprises. These are problems which have been solved before, with limited scope for innovation. For example, handling payments.
The supporting subdomains are activities that support the core domains. For example, the way the products are cataloged does not impact the business as much as the core subdomains.
The domain model comprises list of things (entities, behaviors, rules, etc.) within the subdomain expressed in a very precise language.
This is the shared vocabulary of the domain that all stakeholders (domain experts, software engineers, product managers, etc.) use in communicating with each other.
In any software project, there is a linguistic divide between the domain experts and software developers. Both use the technical jargon of their respective areas, while only vaguely understanding the other. The result is the creation of software that is disconnected from the day to day discussions.
To consider a very trivial example, domain experts may use terms such as user and account interchangeably. However, in reality, they may be very different — each user may have multiple accounts, or vice-versa. Similar ambiguity may exists between products & SKUs, or between various customer progression stages such as suspects, prospects, buyers, repeat buyers, etc.
This is where Ubiquitous Language comes in. When describing the domain, all the stakeholders need to establish a common Ubiquitous Language to do so. This comprises of the names of the prominent entities in the domain (e.g. customer, supplier, prospect, payment, bill, item, SKU, etc.), the behaviors (e.g. purchase, returns, shipment, checkout, stocking, recommendations, etc.), the business rules (e.g. shipment starts after payment is complete, etc.), the large scale structures that bind the model together, the bounded contexts (see below) that define relationships between various subsystems, etc.
The Ubiquitous Language needs to be very precise, since software does not in any way tolerate ambiguity. Everyone commits to using the Ubiquitous Language pervasively as a way of describing the system. As domain experts describe the system, gaps in Ubiquitous Language are quickly identified and filled. Where the descriptions are awkward or wrong, they make changes by experimenting with alternative terms, until the language becomes rich enough to describe what the software must do.
As the scope of the problem domain becomes complex, some objects in the domain have different meaning depending on the context. Therefore it becomes necessary to establish boundaries within which the meaning and behavior are clarified.
For example, the term “user” may refer to a merchant or a customer or a prospect, depending on the context in which we use the term. Another example: “product” may have a different meaning in the billing context (e.g. product price, promotions, etc.) than in the support context (product attributes, warranties, etc.).
One way of resolving these kinds of issues is to create a universal object that explicitly merges all the attributes into a single global definition. However, this is unwieldy and leads to too much coupling. You cannot expect everyone to always use universal names; sometimes domain experts will use “user”, other times they may use “merchant”.
Rather than have a single unified model to represent the domain, DDD recommends the creation of a bounded context which enforces the encapsulation for the model within the boundaries of the context, defining the attributes and behavior for each context.
These domain models may share concepts across the context boundaries (e.g. customer, merchant, product, etc.) and / or they may contain concepts that are only local to the domain (e.g. payment method may only be used in the payment bounded context).
The context boundaries are chosen in a deliberate fashion. For instance, product recommendations can be in its own bounded context, or it can be within the context of the overall merchandizing subdomain. This usually reflects the organization’s communication structure. See Conway’s Law for more information.
Each bounded context has its own architecture and its own presentation layer. The presentation layer from multiple bounded contexts can be pulled together using, for example, a composite pattern. For instance, the storefront of an online commerce platform is a composite of many contexts coming together — search, recommendations, catalog, billing, support etc.
Each bounded context is owned by only one team. However, a team may be responsible for more than one bounded context. The context boundaries define the boundaries and the responsibilities for the teams in the organization.
Collaboration between Bounded Contexts
With a DDD approach, the whole system can be modeled as a set of bounded contexts interacting with each other. A system that has only one bounded context will architecturally devolve into a “Big Ball of Mud”.
A context map is a useful tool to manage this integration / collaboration. A context map should hide the implementation details to avoid tight coupling between contexts, and highlight the interface contracts that enable contexts to integrate with each other.
There are several design patterns for collaboration. Some of the common ones are: partnership, shared kernel, conformist, and anti-corruption layer.
Partnership: This is the simplest approach to collaboration, where teams managing each bounded context have a mutual dependency on each other, and agree to notify each other whenever things change in an informal, ad-hoc fashion. This is useful especially in the early stages of system design when things are simple.
Shared Kernel: In this approach, many teams may share a subset of their domain models and may want to make changes to the shared subset. For instance, “user” in site customer management and “user” in product recommendations. In many cases, the site merchandizing team also manages product recommendations. In such scenarios, the team may create “user” as a shared context. This is both referenced and owned by both the contexts, and any updates are done in a managed fashion.
Conformist: What happens if you use a third party service for authentication and the service decides to make breaking changes in its API? You can do nothing but make changes to your downstream domain model. You have implemented a domain model that’s conformist with the upstream provider’s domain model.
Anti-corruption Layer: In the online commerce scenario, site merchandizing and inventory have a upstream-downstream relationship. So do, orders and delivery. What happens when the downstream domain is dependent on the upstream domain’s interface (e.g. API), and the downstream domain wants to protect its domain model from frequent changes in the upstream domain APIs? The downstream domain can create an anti-corruption layer that translates the supplier’s domain model into something that it needs, thereby isolating the changes to that layer.
There are several other design patterns for collaboration and we have only touched on a few commonly used ones.
In the second part of this series, I will discuss the tactical patterns.