The hardest decision when designing a microservices architecture is not the technology, but where to draw the boundaries: what goes into each service and what stays out. A poor decomposition leads to coupled services that must be deployed together, shared data schemas, and changes that propagate in cascade. A good decomposition produces cohesive, autonomous services that are easy to evolve.
In this lesson we will study the two main decomposition strategies (by business capability and by subdomain), introduce the concept of bounded context from Domain-Driven Design (DDD), and learn how to calibrate the appropriate size and granularity of a service.
Contents
- The problem of boundaries
- Strategy 1: decomposition by business capability
- Strategy 2: decomposition by subdomain (DDD)
- Bounded Contexts and ubiquitous language
- Granularity and appropriate size
- Coupling, cohesion, and the context map
- Common mistakes and tips
- Exercises
- Conclusion
- The problem of boundaries
A poorly placed boundary has an enormous cost: correcting it involves moving data, rewriting APIs, and coordinating several teams. The goal of a good boundary is to maximize cohesion within the service (the things that change together stay together) and minimize coupling between services (changing one does not force you to change others).
| Property | What we seek |
|---|---|
| Cohesion | High: what changes together lives together. |
| Coupling | Low: services depend little on each other. |
| Autonomy | High: each service deploys and operates on its own. |
The golden rule: decompose by business domain, not by technical layer. Services such as "database service", "validation service", or "utilities service" are an antipattern, because no business change is resolved by touching a single service.
- Strategy 1: decomposition by business capability
A business capability is something the organization does to generate value, regardless of how it implements it. They are identified by analyzing what the company does, not what tables it has.
At an insurer, typical capabilities would be:
- Customer management
- Policy underwriting
- Claims handling
- Billing and collections
- Reinsurance
# Each business capability becomes a candidate service
capabilities:
policy_underwriting:
actions: [create_policy, renew_policy, cancel_policy]
published_events: [PolicyCreated, PolicyRenewed, PolicyCancelled]
claims_handling:
actions: [open_claim, appraise, indemnify]
published_events: [ClaimOpened, ClaimIndemnified]Each capability groups the actions and events that belong to it. This strategy is stable, because business capabilities change far less often than technology.
- Strategy 2: decomposition by subdomain (DDD)
Domain-Driven Design decomposes the overall domain into subdomains, which it classifies into three types:
| Subdomain type | Definition | Recommended strategy |
|---|---|---|
| Core | What differentiates the company; its competitive advantage. | Invest the best effort; careful in-house development. |
| Supporting | Necessary but not differentiating. | Simpler development, possible partial outsourcing. |
| Generic | A common problem already solved in the market. | Buy or use a standard solution. |
For an insurer, the actuarial calculation of premiums is probably a core subdomain; document management is supporting; and sending emails is generic (an external provider is used).
This classification guides investments: it makes no sense to build a generic subdomain from scratch when mature solutions exist.
- Bounded Contexts and ubiquitous language
A bounded context is an explicit boundary within which a domain model and its vocabulary have a precise and unique meaning. The central idea is that the same word can mean different things in different contexts.
Consider the term "Customer":
- In Underwriting, a customer is a policyholder with the capacity to contract and a risk profile.
- In Billing, a customer is a debtor with an account and some receipts.
- In Claims, a customer is an injured party or beneficiary.
Forcing a single "Customer" model for all three contexts produces a gigantic class with dozens of fields that no one fully understands. The DDD solution is that each bounded context has its own Customer model, tailored to its needs.
// Same concept, two models depending on the bounded context.
// Underwriting context: we care about risk
package com.fiatc.underwriting;
public class Customer {
private String id;
private RiskProfile riskProfile;
private List<Policy> activePolicies;
}
// Billing context: we care about collections
package com.fiatc.billing;
public class Customer {
private String id; // same identifier to correlate
private PaymentMethod paymentMethod;
private List<Receipt> pendingReceipts;
}The id acts as the link between contexts, but each model only contains what its domain needs. The ubiquitous language is the common vocabulary shared by developers and business experts within a context, and it is reflected directly in the code.
graph LR
subgraph "Underwriting Context"
C1[Customer: policyholder]
end
subgraph "Billing Context"
C2[Customer: debtor]
end
subgraph "Claims Context"
C3[Customer: beneficiary]
end
C1 -. same id .- C2
C2 -. same id .- C3Each bounded context is a natural candidate for a microservice. This is the most reliable rule for drawing boundaries.
- Granularity and appropriate size
How big should a service be? There is no magic lines-of-code metric. Better indicators:
- Functional cohesion: a service should be describable with a single business sentence.
- Transactionality: what needs to be in the same ACID transaction should live in the same service.
- Rate of change: what changes together should be together.
- Team autonomy: a service fits in the head of a small team.
| Symptom | Diagnosis | Action |
|---|---|---|
| Many calls between two services for each operation | Too fragmented | Merge |
| A business change requires touching 5 services | Poorly placed boundaries | Redesign boundaries |
| A service with dozens of disparate responsibilities | Too large | Split |
| You constantly need distributed transactions | Incorrect split | Regroup by consistency |
Practical advice: start with services that are larger than you think you need and split them when real pain appears. It is much easier to split a service than to merge two.
- Coupling, cohesion, and the context map
DDD proposes documenting how contexts relate through a context map. Some relationship patterns:
- Customer/Supplier: one context consumes another's API; the supplier must respect the customer's needs.
- Conformist: the consumer simply adapts to the supplier's model.
- Anticorruption Layer (ACL): the consumer translates the foreign model into its own to avoid contamination.
// Anticorruption layer: translates the external model into the internal model
public class CustomerAcl {
public Customer translate(ExternalCustomerDTO dto) {
// We don't let the external model "leak" into our domain
Customer customer = new Customer();
customer.setId(dto.getCode()); // name mapping
customer.setRiskProfile(calculate(dto)); // semantics adaptation
return customer;
}
}The anticorruption layer is invaluable when integrating legacy systems: it isolates your clean model from the quirks of the external system.
Common Mistakes and Tips
- Decomposing by technical layers (data, validation, logic): any business change touches all services. Decompose by domain.
- A single canonical model for the entire company: produces gigantic, unmanageable models. Adopt bounded contexts.
- Services that are too small from the start: start large and split when real pain appears.
- Ignoring business experts: the correct boundaries emerge from conversations with those who know the domain (techniques such as Event Storming help).
- Sharing tables between contexts: breaks the boundary. Use APIs or events.
Exercises
- Explain in your own words what a bounded context is and why the same word ("Account", "Product"...) may need different models in different contexts.
- Classify the following subdomains of a bank as core, supporting, or generic, and justify: (a) credit scoring engine, (b) document template management, (c) SMS sending.
- You come across two services, "Orders" and "Order Lines", that call each other dozens of times per operation and share transactions. What diagnosis do you make and what action do you take?
Solutions
-
A bounded context is a boundary within which a model and its vocabulary have a unique and coherent meaning. The same word needs different models because each context cares about different attributes and behaviors: "Product" in the catalog carries a description and images, while in the warehouse it carries location and stock. Unifying them creates an overloaded model.
-
(a) Core: scoring differentiates the bank and is its competitive advantage; careful in-house development. (b) Supporting: necessary but not differentiating; simple development. (c) Generic: a solved problem; an SMS provider is contracted.
-
Diagnosis: they are too fragmented; the fragmentation crosses a transactional boundary. Action: merge them into a single "Orders" service that contains the lines as part of its aggregate, eliminating the network calls and enabling local ACID transactions.
Conclusion
We have seen that the key to a good microservices architecture lies in drawing boundaries by business domain, relying on business capabilities and, above all, on the bounded contexts of DDD. The correct granularity seeks high cohesion and low coupling, and it is advisable to start with large services and split them when real pain appears.
Once the services and their boundaries are defined, the practical question arises of how they talk to each other without becoming overly coupled. The next lesson, API Gateway, Service Discovery, and Inter-Service Communication, addresses precisely the mechanisms of synchronous and asynchronous communication, and the infrastructure that makes them possible.
Application Architecture Course
Module 1: Fundamentals of Application Architecture
- What Is Application Architecture?
- The Role of the Software Architect
- Quality Attributes and Non-Functional Requirements
- Architectural Decisions and Trade-offs
- Architecture Documentation: Views and the C4 Model
Module 2: Design Principles and Tactics
- Coupling, Cohesion and Separation of Concerns
- SOLID Principles Applied to Architecture
- DRY, KISS, YAGNI and Other Design Principles
- Architectural Tactics for Quality Attributes
- Managing Technical Debt
Module 3: Architectural Styles and Patterns
- Monolithic Architecture
- Layered Architecture (N-Tier)
- Client-Server Architecture
- Hexagonal Architecture (Ports and Adapters)
- Clean and Onion Architecture
Module 4: Distributed Architectures and Microservices
- Introduction to Distributed Systems
- Microservices Architecture
- Service Decomposition and Bounded Contexts
- API Gateway, Service Discovery and Inter-Service Communication
- Resilience Patterns: Circuit Breaker, Retry and Bulkhead
- The CAP Theorem and Data Consistency
Module 5: Event-Driven Architectures and Messaging
- Fundamentals of Event-Driven Architecture
- Asynchronous Messaging: Queues and Brokers
- Event Patterns: Event Sourcing and CQRS
- Managing Distributed Transactions: The Saga Pattern
- Real-Time Data Streaming
Module 6: Domain-Driven Design (DDD)
- Core DDD Concepts
- Strategic Design: Bounded Contexts and Ubiquitous Language
- Tactical Design: Entities, Aggregates and Repositories
- Context Mapping
Module 7: Data and Persistence
- Persistence Strategies: SQL vs NoSQL
- Data Access Patterns: Repository, Unit of Work and DAO
- Database per Service and Distributed Data Management
- Caching and Invalidation Strategies
Module 8: Cloud Architecture and Deployment
- Cloud Computing Fundamentals (IaaS, PaaS, SaaS)
- Containers and Orchestration with Docker and Kubernetes
- Serverless Architecture
- Cloud-Native Design Patterns
- Infrastructure as Code (IaC)
Module 9: Quality, Security and Observability
- Scalability: Horizontal vs Vertical and Load Balancing
- High Availability and Fault Tolerance
- Security by Design and Authentication/Authorization
- Observability: Logging, Metrics and Tracing
- Performance and Load Testing
