Monorepos allow huge enterprise applications to be subdivided into small and maintainable libraries. This is, however, only one side of the coin: We need to first define criteria for slicing our application into individual parts and we must establish rules for communication between them.
In this article I present a methodology I'm using for subdividing big software systems into individual parts: It's called Strategic Design and it is part of the domain driven design (DDD) approach. In the next article, show how to implement its ideas with an Nx-based monorepo.
What's Domain Driven Design about?
Domain-Driven Design (DDD) provides an approach that bridges the gap between complex business requirements and appropriate application design. A key goal is to align the software’s internal model with the structure and language of the real-world domain it is intended to support. DDD is typically divided into two complementary disciplines: Tactical Design and Strategic Design.
Tactical Design introduces concrete building blocks and patterns for modelling domain logic. These patterns were originally developed with a strong object-oriented mindset. However, the core principles—such as focusing on expressive domain models and preserving business meaning in code—can also be adapted to other paradigms, such as Functional Domain Modeling, which transfers these ideas into the functional programming world.
In contrast, Strategic Design focuses on decomposing large, complex systems into distinct (sub)domains and defining clear bounded contexts for them. This helps manage complexity by ensuring that each part of the system has well-defined boundaries, consistent language, and a clear purpose.
Regardless of one’s preferences regarding implementation style, many of the principles from Strategic Design have proven to be highly effective for structuring systems into smaller separated and maintainable units. This article specifically builds on those strategic concepts and explores how they can be applied in the context of Angular. Whether or not other aspects of DDD are considered is outside the scope of this article.
Discovering Sub Domains with Strategic Design
One of the goals of Strategic Design in Domain-Driven Design is to discover subdomains. Subdomains are distinct areas of the business domain that represent coherent sets of related business capabilities, rules, and terminology. They help to break down the overall problem space into smaller, more manageable parts. Each subdomain typically reflects a specific business responsibility and is often associated with a dedicated group of domain experts.
Identifying subdomains is crucial for defining clear boundaries, prioritizing development efforts, and aligning technical solutions with business value. For instance, an e-procurement system that allows users to request products needed for their work could consist of the following sub domains:
From Sub Domains to Bounded Contexts
While sub domains represent parts of the real world (the "problem space") they are broken down to so called bounded contexts representing them in your solution (the "solution space"). So both are two sides of the same coin. Ideally, each domain has its own bound context. However, we can also decide to break down a sub domain to several bounded contexts.
Within each bounded context, a so-called ubiquitous language is used. This is a shared language between developers and domain experts that reflects the specific terminology, concepts, and rules relevant to that particular context. An example is using the term product in exactly the meaning that makes sense for the ordering context. It ensures that all stakeholders have a common understanding and helps avoid ambiguities when designing, discussing, or implementing the model.
For finding good candidates for a bounded contexts, it is worth taking a look at the processes in the system. For example, an e-Procurement system that handles the procurement of office supplies could support the following two processes:
It is noticeable that the process steps Approve Order
, Request Budget
and Approve Budget
primarily revolve around organizational hierarchies, the available budget and its use. In addition, managers are primarily involved here. By contrast, the process step is primarily about employees that want to request a product.
Identifying Boundaries with Pivotal Events
Another heuristic for finding boundaries between bounded contexts is identifying so-called pivotal events. He defines a pivotal event as an event that "marks a transition between different business phases.". Such an event often causes a meaningful state transition in a central domain concept and can shift responsibilities from one actor to another.
In our example, we can identify such a pivotal event after a user finished a product request. At this point, a key milestone has been reached: the request transitions into the Submitted state. Furthermore, responsibility is handed over to other users of the system—those who are now in charge of specifying the resulting order and approving it.
The idea of Pivotal Events has been defined as part of Alberto Brandolini's work on Event Storming — a workshop format that brings together developers and domain experts to collaboratively explore business processes by mapping domain events along a timeline. As part of this, candidates for bounded contexts are identified and discussed.
Different Models Representing the Real World
Of course, it can be argued that products are omnipresent in an e-Procurement system. However, a closer look reveals that the word product
denotes different things in some of the process steps shown here. For example, while a product is very detailed when it is selected in the catalog, the approval process only needs to remember a few key data:
Hence, a distinction must be made between these two forms of a product. This leads to the creation of different models that are as concrete as possible and therefore meaningful. As our example illustrates, each model is only valid within its bounded context and follows its ubiquitous language.
At the same time, this approach prevents the creation of a single model that attempts to describe the entire world. Such models are often confusing and ambiguous. In addition, they have too many interdependencies that make decoupling and subdividing impossible.
At a logical level, the individual views of the product may still be related. This relationshop can be expressed by using the same id on both sides.
Context-Mapping
Although individual bounded contexts usually separated from eachother, they still have to interact from time to time. In the example considered here, the ordering
context for sending orders could access both the catalog
context and a connected ERP system:
The way these contexts interact with each other is determined by a context map. In principle,
Ordering
and Booking
could share the common model elements. In this case, however, care must be taken to ensure that one modification does not entail inconsistencies on the other.
One context could easily use the other. In this case, however, the question arises as to who needs whom to succeed. Can the consumer impose certain changes on the provider and insist on backward compatibility? Or does the consumer have to be satisfied with what it gets from the provider?
In the case under consideration, Catalog offers an API to prevent changes in the context from forcibly affecting consumers. Since ordering has little impact on the ERP system, it uses an anti-corruption layer (ACR) for access. If something changes in the ERP system, it only has to update this ACR.
Another possible strategy I want to stress out here is Separate Ways
, which means that a specific aspect like calculating the VAT is separately implemented in several bounded contexts:
At first sight this seems to be awkward, especially because it leads to code redundancies and hence breaks the DRY principle (don't repeat yourself). Nevertheless, in some situations it comes in handy because it prevents a dependency to a shared library. While preventing redundant code is an important goal preventing dependencies is as well vital. The reason is that each dependency also defines a contract and contracts are hard to change. Hence, it's a good idea to evaluate whether an additional dependency is worth it.
Besides the patterns shown here, Strategic Design defines further strategies for organizing the relationship between consumers and providers.
Conclusion and Outlook
Strategic Design is about identifying sub domains ("problem space") that are broken down to bounded contexts ("solution space"). In each bounded context we find an ubiquitous language and concepts that only make sense there. A context map shows how those contexts interact with each other.
In the next article we'll see we can implement those contexts with an Angular using an Nx-based monorepo.