Designing Microservices with Domain‑Driven Design, Bounded Contexts, and Event Storming
This article explains how to break a monolithic system into well‑defined microservices by applying domain‑driven design concepts such as bounded contexts and aggregates, using context mapping and event‑storming techniques, and addressing communication, consistency, and orchestration challenges in distributed cloud‑native architectures.
Although the word "micro" in microservices refers to the scale of a service, it is not the only criterion for using microservices. When teams adopt a microservice‑based architecture, they aim to increase agility and deploy features autonomously and frequently. Adrian Cockcroft defines microservices succinctly as "a service‑oriented architecture composed of loosely coupled elements with contextual boundaries."
Beyond this high‑level heuristic, microservice architecture has distinct characteristics that set it apart from traditional service‑oriented architectures. Notable features include:
Services have clearly defined boundaries around business contexts rather than arbitrary technical abstractions.
Implementation details are hidden behind intent‑driven interfaces that expose only functionality.
Services do not share internal structures beyond their boundaries, e.g., they do not share databases.
Services are fault‑tolerant.
Teams own their services end‑to‑end and can release changes independently.
Teams embrace automation, such as automated testing, continuous integration, and continuous delivery.
In short, the style can be summarized as a loosely coupled service‑oriented architecture where each service lives within a well‑defined bounded context, enabling rapid, frequent, and reliable delivery of applications.
Domain‑Driven Design (DDD) and Bounded Contexts
The power of microservices comes from clearly defining responsibilities and drawing boundaries. High cohesion within a boundary and low coupling across boundaries are essential. DDD provides a set of ideas, principles, and patterns that help design software systems based on a business domain model. Developers and domain experts collaborate using a ubiquitous language to create the model, bind it to meaningful systems, and establish collaborative contracts between teams.
Microservice design draws inspiration from these concepts because they help build modular systems that can evolve independently.
Before proceeding, here are some basic DDD terms (the full overview is beyond this blog’s scope; readers are encouraged to read Eric Evans’s book):
Domain : Represents the work of an organization, e.g., retail or e‑commerce.
Sub‑domain : A business department within the organization; a domain consists of multiple sub‑domains.
Ubiquitous Language : A common language used to express the model; all stakeholders agree on terms such as Item across contexts.
Bounded Context : A setting where a word or phrase has a specific meaning, defining the model’s boundary. For example, Item means a sellable product in a catalog context, a cart entry in a shopping‑cart context, and a shipment item in a shipping context.
Note the distinction between sub‑domains (problem space) and bounded contexts (solution space). A sub‑domain may have multiple bounded contexts, though we aim for one bounded context per sub‑domain.
How Microservices Relate to Bounded Contexts
Each bounded context can map to a microservice, but sometimes a bounded context is large and may contain several aggregates. The goal is to keep aggregates cohesive and isolated, exposing only their root for state changes.
Aggregates are clusters of related objects treated as a single unit of data change. External code may only reference the aggregate root, and consistency rules apply within the aggregate’s boundary.
It is not mandatory to model every aggregate as a separate microservice; multiple aggregates can reside in one service when business understanding is incomplete.
Context Mapping – Precisely Defining Service Boundaries
When decomposing a monolith, identifying models and their relationships is crucial. Context mapping helps identify and define the relationships between bounded contexts and aggregates. For example, in an e‑commerce ordering system, the pricing bounded context includes price, pricing item, and discount models. Modeling all these in a single monolith leads to a large, tangled application.
Separating these models into aggregates and mapping them to appropriate contexts reduces coupling and improves cohesion.
Event Storming – Another Technique for Identifying Service Boundaries
Event storming is a collaborative workshop where teams brainstorm domain events and processes, uncovering overlapping concepts, ambiguous language, and conflicting workflows. The outcome includes a redefined list of aggregates (potential new microservices), the domain events that flow between them, and the commands that external applications or users invoke.
Communication Between Microservices
When a monolith is split, transactions that were once atomic become distributed across many services, making strong consistency expensive. Designers often favor eventual consistency, using asynchronous communication to improve availability.
CAP theorem applies: a distributed system can provide only two of consistency, availability, and partition tolerance. In practice, partition tolerance is non‑negotiable, so a trade‑off between consistency and availability must be made.
Designing for eventual consistency involves making processes asynchronous (e.g., sending emails after order placement) and tolerating brief inconsistencies to maintain high availability.
Supporting Event‑Driven Architecture
Microservices can emit domain events when their aggregates change. Other services listen to these events and react within their own domains, eliminating tight coupling and time‑coupling between services.
Producers should guarantee at‑least‑once delivery and provide fallback mechanisms.
Consumers must handle events idempotently; they may use timestamps or version fields to ensure uniqueness.
When synchronous integration is required (e.g., Cart calling Payment), consider converting to event‑driven integration, using batch jobs, or generating local events before invoking the external API.
Avoiding Orchestration for Specific Consumer Data Needs
One anti‑pattern is building services that satisfy specific consumer access patterns, leading to tightly coupled APIs across aggregates. Instead, let consumer teams own orchestration via a Backend‑for‑Frontend (BFF) that aggregates data from multiple services, possibly using GraphQL for flexible queries.
Conclusion
This blog touched on concepts, strategies, and design heuristics for transitioning from a monolithic system to domain‑driven microservices. While each topic could be explored in depth, the article highlighted key ideas and practical experiences that helped shape the author’s approach.
IT Architects Alliance
Discussion and exchange on system, internet, large‑scale distributed, high‑availability, and high‑performance architectures, as well as big data, machine learning, AI, and architecture adjustments with internet technologies. Includes real‑world large‑scale architecture case studies. Open to architects who have ideas and enjoy sharing.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.