AlgoInsight

Free lesson · Microservices Interview

Domain-Driven Design Essentials for Microservices

Easy10 min readmicroservicessystem-designdistributed-systems

Domain-Driven Design Essentials for Microservices

Domain-Driven Design is a massive topic. Eric Evans' original book is 560 pages. Vaughn Vernon's follow-up is another 656. Most engineers who say they "know DDD" have read a blog post about bounded contexts and aggregates. That is not a criticism - it is an observation about where the practical value lives.

For microservices decomposition, you do not need all 560 pages. You need three concepts deeply: bounded contexts, aggregates, and ubiquitous language. These three are the tools that turn vague decomposition intuitions into defensible service boundary decisions. Everything else in DDD - entities, value objects, domain events, repositories, factories, specifications - is useful for implementation within a service but does not change where you draw the boundaries.

This is a deliberate scoping decision that is itself worth understanding. DDD has strategic patterns (bounded contexts, context maps, subdomains) and tactical patterns (entities, value objects, repositories, domain services). Microservices decomposition is a strategic question. The tactical patterns matter for implementation after the boundaries are drawn. Spending interview time on tactical DDD when the interviewer asked a strategic decomposition question is a misallocation of your most constrained resource - time.

This lesson gives you those three concepts at the depth required for system design interviews. Not a DDD survey course. Not a theoretical treatment. The minimum sufficient DDD to decompose a system into microservices and defend your decisions under questioning.

Why DDD Matters for Microservices

The connection between DDD and microservices is not incidental. It is structural.

Microservices require you to draw boundaries - to decide what belongs together in one service and what belongs in a separate service. If you draw boundaries arbitrarily (by technical layer, by team familiarity, by what seems "small enough"), you will get boundaries that create excessive inter-service communication, data consistency problems, and teams that cannot ship independently because their services are coupled to everyone else's.

DDD provides a principled method for drawing boundaries based on the domain itself - the business problem the software is solving. The insight is that the domain already has natural seams. Concepts that belong together in the business belong together in the code. Concepts that the business treats as separate should be separate in the architecture. DDD gives you the vocabulary and tools to find those seams.

Important: DDD is not about implementation patterns inside services. In the context of microservices decomposition, DDD is about where to draw the lines, not how to implement what is inside them. A service can use DDD tactical patterns internally (entities, repositories, domain events) or it can use a simple CRUD architecture. The decomposition question - which is what interviews test - is about the strategic patterns: bounded contexts, context mapping, and subdomains.

Bounded Contexts - The Core Concept

A bounded context is the most important DDD concept for microservices. It is, in fact, the concept that microservices service boundaries map to most directly.

What a Bounded Context Is

A bounded context is a linguistic boundary within which a particular model of the domain is consistent and unambiguous. Inside a bounded context, every term has one meaning. "Customer" means one specific thing. "Order" means one specific thing. "Product" means one specific thing.

Across bounded contexts, the same word can mean different things. And this is the key insight: they should mean different things, because different parts of the business think about these concepts differently.

Consider an e-commerce company. The word "Product" means something different depending on who is using it:

graph TD
    subgraph Catalog["Catalog Context"]
        P1["Product:\nname, description,\nimages, categories,\nreviews"]
    end
    
    subgraph Inventory["Inventory Context"]
        P2["Product:\nSKU, warehouse location,\nquantity, reorder\nthreshold"]
    end
    
    subgraph Pricing["Pricing Context"]
        P3["Product:\nbase price, discounts,\nregional pricing,\npromotions"]
    end
    
    subgraph Shipping["Shipping Context"]
        P4["Product:\nweight, dimensions,\nfragility, shipping\nclass"]
    end
This diagram shows how the concept of Product has different meanings in four bounded contexts. In the Catalog context, Product has name, description, images, and reviews. In Inventory, Product has SKU, warehouse location, and quantity. In Pricing, Product has base price and discount rules. In Shipping, Product has weight, dimensions, and fragility. The same word refers to different models in each context.

These are not four views of one thing. They are four genuinely different models, each optimised for the work that context needs to do. The Catalog context does not need to know about warehouse locations. The Shipping context does not need to know about reviews. Forcing all four into a single "Product" model creates a bloated, compromised data structure that serves none of the contexts well.

Bounded Contexts Map to Services

The mapping from bounded contexts to microservices is close to one-to-one. Each bounded context becomes a candidate for a separate service. The Catalog service owns the catalog model of Product. The Inventory service owns the inventory model. They share a product identifier (a SKU or product ID) but not a data model.

This is why bounded contexts are the most important DDD concept for microservices. They answer the fundamental question: what belongs together? The answer is: things that share a model. Things that use the word "Product" to mean the same thing. Things that, if you changed the model for one use case, would need to change for the other use cases too. Those belong in one bounded context, and therefore one service.

Things that use the word "Product" to mean different things - different attributes, different invariants, different lifecycles - belong in separate bounded contexts and separate services. Putting them together would create coupling through a shared model that no one context fully owns.

Interview: How to use bounded contexts in an interview: When decomposing a system, explicitly name the bounded contexts before drawing service boxes. "I see four distinct bounded contexts here: Catalog, Inventory, Pricing, and Shipping. Each has its own model of Product with different attributes and different invariants. I would make each a separate service because they can evolve independently - changing the pricing model should not require changes to the catalog service." This demonstrates that your decomposition is principled, not arbitrary.

Aggregates - The Consistency Boundary

If bounded contexts tell you where to draw service boundaries, aggregates tell you what constitutes a transactional unit within a service. An aggregate is a cluster of domain objects that are treated as a single unit for data changes.

What an Aggregate Is

An aggregate has a root entity (the aggregate root) and a set of related entities and value objects that must be consistent together. Changes to the aggregate go through the root. The aggregate enforces its own invariants - business rules that must always be true.

The critical rule is: a transaction should modify only one aggregate. If a business operation needs to modify two aggregates, it should be modeled as two separate transactions coordinated through a domain event or a saga - not as one transaction that spans both.

For example, in an Order context:

When a customer places an order, two aggregates are involved: Order (created) and Inventory (decremented). These should be two separate transactions. The Order service creates the order. The Inventory service decrements stock. If the inventory decrement fails, the system handles it through compensation (cancel the order) rather than through a cross-aggregate transaction.

graph TD
    subgraph OrderAgg["Order Aggregate"]
        OR["Order\n(Aggregate Root)"]
        LI1["Line Item 1"]
        LI2["Line Item 2"]
        LI3["Line Item 3"]
        OR --> LI1
        OR --> LI2
        OR --> LI3
    end
    
    subgraph CustomerAgg["Customer Aggregate"]
        C["Customer\n(Aggregate Root)"]
        A["Address"]
        PM["Payment Method"]
        C --> A
        C --> PM
    end
    
    OR -.->|"references by ID"| C
This diagram shows two aggregates. The Order aggregate has an Order root entity containing three Line Items - these form a transactional unit. The Customer aggregate has a Customer root entity containing Address and Payment Method. The Order references the Customer by ID but does not contain it, meaning they are separate transactional units.

Why Aggregates Matter for Microservices

Aggregates matter for decomposition because they define the minimum unit that must live together. You can split things that are in different aggregates into different services. You should not split things that are in the same aggregate into different services - because they need transactional consistency, and cross-service transactions are exactly what microservices make hard.

The practical implication: when identifying service boundaries, check that each service contains complete aggregates. If a proposed service boundary cuts through the middle of an aggregate - putting the Order in one service and its Line Items in another - that boundary is wrong. Those two things need atomic consistency, and splitting them across a network boundary means you need a distributed transaction to maintain it.

Warning: Aggregate boundaries that are too large are a common mistake. If your Order aggregate contains the Order, its Line Items, the Customer, the Payment Transaction, and the Shipping Record, your aggregate is too big. Changes to shipping status would lock the entire order for modification. The discipline is to make aggregates as small as possible while still preserving the invariants that matter. Only things that must be atomically consistent belong in the same aggregate.

Ubiquitous Language - The Communication Tool

Ubiquitous language is the practice of using the same terminology in code, in conversation, and in the domain. The terms used by the business should be the terms used in the codebase - not translated into technical jargon.

Why It Matters for Decomposition

Ubiquitous language matters for microservices decomposition because linguistic boundaries reveal domain boundaries. When different teams or different parts of the business use different vocabularies - or worse, use the same words to mean different things - those differences mark the seams where bounded contexts should be separated.

If the warehouse team calls something a "unit" and the marketing team calls the same physical thing a "product" and the finance team calls it a "line item," those are three bounded contexts with three different models. The terminology difference is not confusion to be resolved by picking one word - it is a domain signal that these teams think about the concept differently because they do different work with it.

In interviews, ubiquitous language shows up as a decomposition validation tool. After you propose your bounded contexts, check: does each context have a consistent vocabulary? Are there terms that mean different things across contexts? If "order" means "a customer's purchase request" in one context and "a warehouse pick instruction" in another, those should be separate contexts - even though both are called "order."

Tip: In interviews, naming your services using domain language - not technical language - signals DDD awareness. "Order Management Service" is better than "CRUD Service for Orders." "Fulfillment Service" is better than "Backend Processing Service." "Pricing Engine" is better than "Calculator Service." The names should come from the domain, not from the implementation.

What You Do Not Need from DDD (For Decomposition)

DDD has many more concepts than the three above. For decomposition purposes - which is what system design interviews test - you do not need deep knowledge of:

These concepts matter when you are building the service. They do not matter when you are deciding which services to build. In an interview, spending five minutes explaining the Repository pattern is time you should have spent justifying your service boundaries.

Applying DDD to Decomposition - A Worked Example

Let me walk through how these concepts apply to a concrete decomposition problem. Consider a ride-sharing platform.

Step 1 - Identify the ubiquitous language. Different parts of the business use different vocabularies:

Step 2 - Identify bounded contexts. Each vocabulary cluster represents a bounded context:

Step 3 - Identify aggregates within each context. The Ride Context has a Ride aggregate (ride request, pickup, route, dropoff - these must be consistent). The Driver Context has a Driver aggregate (profile, availability status, current location, earnings - updated atomically). Payment has a Transaction aggregate.

Step 4 - Validate boundaries. Check each proposed boundary: can each context operate mostly independently? Does each context have its own model of shared concepts? "Driver" in the Ride Context is a driver ID and a current location. "Driver" in the Driver Context is a full profile with earnings history, shift schedule, and vehicle details. These are different models - the boundary is valid.

Also check for coupling signals. If two proposed contexts would need to call each other synchronously for every operation, they may be too tightly coupled to separate. The Matching Context needs the Driver Context's availability data and the Ride Context's pickup location - but it can receive these through events or cached snapshots rather than synchronous calls. That is loose coupling. If Matching could not function without a real-time synchronous call to Driver on every match attempt, you would reconsider whether Matching and Driver should be separate contexts.

The output of this process is not just a list of services. It is a list of services with justification - each boundary supported by linguistic differences, model independence, and aggregate boundaries. That justification is what the interviewer is evaluating.

Interview: This four-step process is the framework to use in interviews. When asked to decompose a system: (1) identify the language clusters, (2) propose bounded contexts, (3) identify the key aggregates, (4) validate that each context can operate independently. Walking through these steps explicitly shows the interviewer a principled decomposition methodology rather than an ad-hoc list of services.

What Comes Next

This lesson gave you the DDD tools for thinking about decomposition. The next lesson takes those tools and applies them to the specific question of how to identify where service boundaries should be drawn - using coupling, cohesion, and data ownership as the primary signals.

Back to Microservices Interview