Defining Service Boundaries: Domain-Driven Design
Learn how to decompose a monolith into microservices using bounded contexts, domain-driven design principles, and practical decomposition strategies.
Defining Service Boundaries: Domain-Driven Design
Service boundaries matter more than almost any other decision in a distributed system. Get them right and your services are loosely coupled, independently deployable, and easy to reason about. Get them wrong and you end up with a distributed monolith, where deploying one service requires deploying several others, which defeats the entire point of decomposition.
Domain-Driven Design (DDD) gives us tools for thinking through this problem. The most useful concept is the bounded context, which I will focus on in this post.
What is a Bounded Context?
A bounded context is a linguistic and organizational boundary within which a particular domain model applies. Inside the boundary, every term, concept, and business rule has a specific, consistent meaning. Outside the boundary, that meaning may differ.
Think about the word “order.” In an e-commerce bounded context, an order is a customer request to purchase products. In a shipping bounded context, an order is a container being transported from port to port. These are not the same thing, even though they share a name. Each bounded context maintains its own model of reality.
This distinction prevents the kind of conceptual leakage that turns monoliths into tangled messes. When the shipping context changes how it handles orders, the e-commerce context should not need to change. They are independent.
Bounded Contexts in a Monolith
Even in a monolith, bounded contexts exist conceptually. The problem is that they are often violated. One function in the orders module directly updates a column in the inventory table. The user service knows the internal structure of the account table. The payment module calls into the notification module as though it were just another function.
These violations create hidden dependencies. Deploying the orders module means considering how the inventory table might be affected. A monolith is not one big ball of confusion; it is a collection of poorly isolated bounded contexts tangled together over time.
Decomposing into services means making these implicit boundaries explicit and enforcing them through physical separation.
How to Identify Domain Boundaries
Identifying bounded contexts is not a purely technical exercise. It requires understanding the business domain deeply. Here are some practical approaches.
Ubiquitous Language
The most reliable signal of a bounded context is a shift in the ubiquitous language. This is the shared vocabulary a team uses to describe the domain. If you catch yourself saying “the same” word but meaning different things in different parts of the system, you are likely looking at two bounded contexts.
For example, a “catalog” in the product context refers to the structured listing of items available for sale. In the marketing context, “catalog” might refer to a promotional PDF sent to customers. These are not the same concept, even though they share a word.
When stakeholders use different words for the same thing, or the same word for different things, that is a signal to investigate the boundary.
Change Patterns
Another practical approach is to look at what changes together and what changes independently. If modifying the pricing logic in your order context requires you to also modify something in the inventory context, those two things are probably in the same bounded context (or are too tightly coupled, which is a problem to fix).
Conversely, if the team that owns the user profile is completely separate from the team that owns the recommendation engine, and they rarely need to coordinate, those are likely separate bounded contexts.
Domain Experts
Talk to domain experts. Not just to gather requirements, but to understand how they think about the domain. When a domain expert describes a workflow, notice where they draw lines. When they say “we handle that separately” or “that is a different team problem,” they are usually describing a bounded context boundary.
The Single Responsibility Principle for Services
The Single Responsibility Principle (SRP) states that a class should have only one reason to change. Applied to services, SRP means a service should have only one job, and that job should be bounded by a single domain concept.
This is subtly different from the idea that a service should be “small.” A service with ten endpoints might still follow SRP if all ten endpoints deal with the same domain concept. A service with two endpoints might violate SRP if those endpoints deal with completely unrelated concepts.
The question to ask is not “how big is this service?” but rather “how many reasons does this service have to change?” Every reason to change is a potential source of coupling.
What is Not a Reason to Change
Some things that seem like reasons to change are actually implementation details that should not influence your service boundary.
Performance requirements are a common example. “The inventory check needs to be fast” is not a reason to separate inventory into its own service. Performance optimizations like caching and indexing happen within a bounded context, without changing the service boundary.
Scaling requirements alone do not justify splitting a bounded context either. If the user service needs to scale differently than the order service, that is a deployment topology question, not a domain decomposition question.
Decomposition Strategies
When you are ready to decompose a monolith into services, you have several strategies to choose from. Each has trade-offs.
Decompose by Noun
The most common approach is decomposing by noun, also called decomposing by business entity. Each major entity in the domain becomes its own service.
In an e-commerce system, this produces services like order service, product service, customer service, and payment service. Each service owns its own data and exposes an API for other services to interact with it.
This approach is intuitive and maps well to how most teams think about their systems. The risk is that it can produce services that are too coarse-grained, especially if the “noun” is actually a cluster of related concepts.
Decompose by Verb
Decomposing by verb produces services organized around business operations rather than entities. Instead of an order service and a payment service, you might have a checkout service that orchestrates the entire checkout process.
This approach is common in workflows that are tightly coupled by nature. Checkout involves validating the cart, reserving inventory, processing payment, and confirming the order. Doing all of that through multiple service calls adds latency and complexity.
The tradeoff is that verb-based services can become coordination-heavy, essentially recreating the monolith logic inside a single service.
Decompose by Subdomain
DDD concept of a subdomain offers a more nuanced approach. A subdomain is a smaller, complete domain model that represents a part of the overall business domain.
In e-commerce, the subdomains might include:
- The core domain, which is the unique Differentiating Core (the thing the business does better than anyone else)
- The supporting subdomains, which are needed to support the core domain but are not differentiating
- The generic subdomains, which could be bought off-the-shelf or outsourced
An e-commerce company might have “order management” as its core domain, “inventory” as a supporting subdomain, and “authentication” as a generic subdomain. The core domain gets the most attention and investment. Supporting subdomains get only what they need. Generic subdomains might be replaced by third-party services entirely.
Case Study: E-Commerce Domain
Let us apply these concepts to a typical e-commerce domain.
The Bounded Contexts
A well-designed e-commerce system might have the following bounded contexts, each becoming a service.
Catalog Context. Manages the product catalog: descriptions, images, categories, pricing rules, and search. This context is read-heavy and benefits from caching.
Inventory Context. Tracks stock levels, reservations, and warehouse allocations. This context has strong consistency requirements because you cannot sell what you do not have.
Order Context. Manages customer orders from creation to fulfillment. This context cares about order lifecycle states, pricing at time of purchase, and fulfillment workflows.
Payment Context. Handles payment processing, refunds, and billing. This context is often isolated for compliance reasons (PCI-DSS) and might be a third-party service.
Customer Context. Manages customer profiles, addresses, authentication, and preferences. This context is often shared across multiple applications beyond e-commerce.
How They Interact
graph TD
Customer[Customer Context] --> Order[Order Context]
Catalog[Catalog Context] --> Order[Order Context]
Inventory[Inventory Context] --> Order[Order Context]
Payment[Payment Context] --> Order[Order Context]
Order[Order Context] --> Notification[Notification Context]
Inventory[Inventory Context] --> Warehouse[Warehouse Context]
Notice that each context has a clear owner and a clear purpose. The order context does not own customer data; it stores a reference to the customer context. It does not own inventory; it asks the inventory context to reserve stock. It does not process payments; it delegates to the payment context.
This loose coupling is what makes independent deployment possible. The customer context can change its data model without affecting the order context, as long as the interface remains compatible.
Coupling Metrics to Watch
When you have decomposed your system, how do you know if the boundaries are correct? Here are some metrics to track.
Inter-Service Dependencies
Count how many services depend on each other. A service with many inbound dependencies is a hub, and hubs are fragile. If that hub goes down, many other services are affected. Consider whether the hub should be split.
Similarly, count how many outbound dependencies each service has. A service with many outbound dependencies is sensitive to changes in many places. It may be doing too much coordination.
Shared Data Stores
If two services directly read from or write to the same database, they are implicitly coupled. A schema change in one service can break another. This coupling should be made explicit (through an API) or eliminated (through separate data stores).
Cyclic Dependencies
A cyclic dependency occurs when service A depends on service B, and service B depends on service A. Cycles make systems rigid and hard to deploy. If you find a cycle, break it by introducing a new service or inverting a dependency.
Common Mistakes
The Distributed Monolith
The most common mistake is creating a distributed monolith. This happens when you decompose a monolith into services but maintain synchronous, blocking dependencies between them. When a change to one service requires simultaneous deployment to several others, you have not gained much over the monolith.
The telltale sign is when your deployment pipeline must coordinate multiple services at once. In a properly decomposed system, each service can be deployed independently.
God Services
The opposite mistake is creating god services, where one service does too much. A service that handles authentication, authorization, user profiles, preferences, and analytics has too many reasons to change. Split it along those lines.
Shared Libraries as Coupling
Be careful about shared libraries. A shared library of domain objects sounds like a good idea until two services that use different versions of that library need different behavior. If you share code, share it at the interface level, not the implementation level. Each service should own its own domain model, even if the models are similar.
Conclusion
Defining service boundaries is one of the hardest problems in distributed systems. There is no formula that works every time. It requires understanding the business domain deeply, talking to domain experts, and making judgment calls that will shape your system for years.
Bounded contexts give you a vocabulary for thinking about boundaries. The Single Responsibility Principle gives you a test for whether a boundary makes sense. Decomposition strategies give you a starting point for exploration.
Microservices are not the goal. A system that can evolve, deploy independently, and remain maintainable as it grows is the goal. Getting the boundaries right is how you get there.
If you want to go deeper, read about the Saga Pattern for managing distributed transactions across service boundaries, or Event-Driven Architecture for communication patterns between bounded contexts. For a broader view of decomposition strategies, see the Microservices Architecture Roadmap.
Category
Related Posts
Amazon's Architecture: Lessons from the Pioneer of Microservices
Learn how Amazon pioneered service-oriented architecture, the famous 'two-pizza team' rule, and how they built the foundation for AWS.
Asynchronous Communication in Microservices: Events and Patterns
Deep dive into asynchronous communication patterns for microservices including event-driven architecture, message queues, and choreography vs orchestration.
Client-Side Discovery: Direct Service Routing in Microservices
Explore client-side service discovery patterns, how clients directly query the service registry, and when this approach works best.