8 Jul 2022 · Software Engineering

    Domain-Driven Design Principles for Microservices

    9 min read
    Contents

    Microservices are the most scalable way of developing software. But you need a good design that lets developer teams work autonomously and deploy without stepping on each other’s toes, otherwise you most lose of the scalability benefits.

    Domain-Driven Development allows us to plan a microservice architecture by decomposing the larger system into self-contained units, understanding the responsibilities of each, and identifying their relationships. In this article, we’ll learn the basics of Domain-Driven Design and how to apply it to microservices.

    What is Domain-Driven Design?

    Domain-Driven Design (DDD) is a software design method wherein developers construct models to understand the business requirements of a domain. These models serve as the conceptual foundation for developing software.

    According to Eric Evans, author of Domain-Driven Design: Tackling Complexity in the Heart of Software, a domain is:

    A sphere of knowledge, influence, or activity. The subject area to which the user applies a program is the domain of the software.

    How well one can solve a problem is determined by one’s capacity to understand the domain. Developers are smart people, but they can’t be specialists in all fields. They need to collaborate with domain experts to guarantee that the code is aligned with business rules and client needs.

    Developers, domain experts and business analyst brainstorm the design and share knowledge. They define an ubiquitious language to use inside each context.
    Developers and domain experts use a unified language to share knowledge, document, plan, and code.

    Caption: Developers and domain experts use a unified language to share knowledge, document, plan, and code.

    The two most important DDD concepts for microservice architecture are: bounded contexts and context maps.

    Bounded Context (BC)

    The setting in which a word appears determines its meaning. Depending on the context, “book” may refer to a written piece of work, or it may mean “to reserve a room”. A bounded context (BC) is the space in which a term has a definite and unambiguous meaning.

    Before DDD it was common practice to attempt to find a model that spanned the complete domain. The problem is that the larger the domain, the more difficult it is to find a consistent and unified model. DDD’s solution is to identify BCs so that the domain can be broken down into manageable subdomains.

    The same concept can have different meanings in different bounded context. A book in the catalog context has an author, cover art, a price and reviews. On the shipment context, none of that matters, only the destination address, and the book's weight.
    The relevant properties of the “book” change from context to context

    In software, we need to be exact. That is why defining BCs is critical: it gives us a precise vocabulary, called ubiquitous language, that can be used in conversations between developers and domain experts. The ubiquitous language is present throughout the design process, project documentation, and code.

    Context Map

    The presence of a BC anticipates the need for communication channels. For instance, if we’re working in an e-commerce domain, the salesperson should check with inventory before selling a product. And once it’s sold, it’s up to shipping to ensure delivery of the product to the correct address. In DDD, these relationships are depicted in the form of a context map.

    Bounded context use communication to achieve a higher task
    Bounded context communication used to achieve a high-level task.

    Domain-Driven Design for microservices

    DDD takes place in two phases:

    1. In the strategic phase we identify the BCs and map them out in a context map.
    2. In the tactical phase we model each BC according to the business rules of the subdomain.

    Let’s see how each phase plays a role in microservice architecture design.

    Strategic phase

    During this phase, we invite developers, domain experts, product owners, and business analysts to brainstorm, share knowledge and make an initial plan. With the aid of a facilitator, this can take the form of an Event Storming workshop session, where we build models and identify business requirements starting from significant events in the domain.

    An event storming session in progress. Post it notes and a whiteboard are used to map out important events for the system.
    An Event Storming session, domain events are used as the catalyst for sharing knowledge and identifying business requirements.

    In strategic DDD, we take a high-level, top-to-bottom approach to design. We begin by analyzing the domain in order to determine its business rules. From this, we derive a list of BCs.

    Strategic Domain-Driven Design overview: Analyze the Domain, then Define Bounded Contexts, and finally identify microservices
    Strategic Domain-Driven Design helps us identify the logical boundaries of individual microservices.,

    The boundaries act as natural barriers, protecting the models inside. As a result, every BC represents an opportunity to implement at least one microservice.

    Bounded contexts must communicate

    Types of relationships

    Next, we must decide how BCs will communicate. Eric Evans lists seven types of relationships, while other authors list six of them. Regardless of how we count them, at least three (shared kernel, customer/supplier, and conformist) imply tight coupling, which we do not want in a microservice design and can be ignored. That leaves us with four types of relationships:

    • Open Host Service (OHS): the service provider defines an open protocol for others to consume. This is an open-ended relationship, as it is up to the consumers to conform to the protocol.
    • Published Language (PL): this relationship uses a well-known language such as XML, JSON, GraphQL, or any other fit for the domain. This type of relationship can be combined with OHS.
    • Anticorruption Layer (ACL): this is a defensive mechanism for service consumers. The anti-corruption layer is an abstraction and translation wrapping layer implemented in front of a downstream service. When something changes upstream, the consumer service only needs to update the ACL.
    • Separate ways: this happens when integration between two services is found, upon further analysis, to be of little value. This is the opposite of a relationship — it means that the BCs have no connection and do not need to interact.

    At the end of our strategic DDD analysis, we get a context map detailing the BCs and their relationships.

    Relationships between bounded contexts are mapped in a context map
    ACL is implemented downstream to mitigate the impact of upstream changes. OHS does the opposite. It’s implemented upstream to offer a stable interface for services downstream.

    Tactical phase

    Deep down, software development is a modeling exercise; we describe a real-life scenario as a model and then solve it with code. In the previous stage, we identified BCs and mapped their relationships. In this stage, which requires developers well-versed in DDD theory, we’ll zoom in on each context to construct a detailed model.

    The models created with DDD are technology-agnostic — they do not say anything about the stack underneath. We focus, instead, on modeling the subdomain. The main building block of our models are:

    • Entities: entities are objects with an identity that persists over time. Entities must have a unique identifier (for example, the account number for a customer). While entity identifiers may be shared among context boundaries, the entities themselves don’t need to be identical across every BC. Each context is allowed to have a private version of a given entity.
    • Value objects: value objects are immutable values without identity. They represent the primitives of your model, such as dates, times, coordinates, or currencies.
    • Aggregates: aggregates create relationships between entities and value objects. They represent a group of objects that can be treated as a single unit and are always in a consistent state. For example, customers place orders and own books, so the entities customer, order, and book can be treated as an aggregate. Aggregates must always be referenced by a main entity, called the root entity.
    • Domain services: these are stateless services that implement a piece of business logic or functionality. A domain service can span multiple entities.
    • Domain events: essential for microservice design, domain events notify other services when something happens. For instance, when a customer buys a book, a payment is rejected, or that a user has logged in. Microservices can simultaneously produce and consume events from the network.
    • Repositories: repositories are persistent containers for aggregates, typically taking the form of a database.
    • Factories: factories are responsible for creating new aggregates.
    The package aggregate model
    The shipping aggregate consists of a package containing books shipped to an address.

    Domain-Driven Design is iterative

    While it may appear that we must first write an exhaustive description of the domain before we can begin working on the code, the reality is that DDD, like all software design, is an iterative process.

    On paper, bounded contexts and context maps may appear OK, but when implemented, they may translate into services that are too big to be rightly called microservices. Conversely, chatty microservices with overlapping responsibilities may need to be merged into one.

    As development progresses and you have a better understanding of the domain, you’ll be able to make better judgments, enhance models, and communicate more effectively.

    More ways of designing microservices

    DDD is undoubtedly a theory-heavy design pattern. As a result, it is only recommended when the system under development is complex enough to warrant the extra planning work.

    Other methods such as Test-Driven Development (TDD) or Behavior-Driven Development (BDD) may be enough for smaller, simpler systems. TDD is the fastest to start with and works best when working on single microservices or even with applications consisting of only a few services.

    On a bigger scale, we can use BDD, which forces us to validate the wholesale behavior with integration and acceptance tests. BDD may work well if you work on low to medium-complexity designs, but once you hit a certain threshold, maintaining the tests can slow you down.

    You can also combine these three patterns, choosing the best one for each stage of development. For example:

    1. Identify microservices and their relationships with strategic DDD.
    2. Model each microservice with tactical DDD.
    3. Since each team is autonomous, they can choose to adopt BDD or TDD (or a mix of both) for developing a microservice or a cluster of microservices.

    Learn more about Domain-Driven Design

    DDD can feel dauting to learn and implement, but its value for developing a microservice architecture is well worth the effort. If you found the information in this article interesting, I recommend picking up the relevant books by Eric Evans and Vaughn Vernon to learn more.

    Continue learning about microservices:

    2 thoughts on “Domain-Driven Design Principles for Microservices

    1. thanks for the write up. I found this to be more easily understandable than so many other articles on DDD!

    2. Respectfully disagree with the idea “once you reach a threshold, tests can slow you down.” Difficult to maintain tests almost always are a tell that something is wrong with the software design. Writing pure functions without side effects are rarely hard to maintain. Algorithms often depend on properties without regard to where they live in the model. As the model evolves, computations based on properties shouldn’t change. If they algorithms change, that shouldn’t rip back up through the model. “Thresholds” are usually hit when a team makes heavy use of mocking or always passing around coarse grained data that is constantly changing. That is almost always a code smell.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    I picked up most of my skills during the years I worked at IBM. Was a DBA, developer, and cloud engineer for a time. After that, I went into freelancing, where I found the passion for writing. Now, I'm a full-time writer at Semaphore.
    Avatar
    Reviewed by:
    I picked up most of my soft/hardware troubleshooting skills in the US Army. A decade of Java development drove me to operations, scaling infrastructure to cope with the thundering herd. Engineering coach and CTO of Teleclinic.