Microservice Decomposition: An Engineering Decision Framework
Breaking a monolith into microservices is not about "making it smaller" — it is about splitting it right, and splitting it only when the cost is worth it. This article gives a decision framework covering when to decompose, how to cut boundaries, how fine is fine enough, and the governance traps that follow — for architects and tech leads evaluating or already on the microservice path.
The Bottom Line: Microservices Are Not a Goal — They Are a Trade-Off
Many teams treat “going microservices” as the terminal of technical evolution. It is not. Microservices are a price you pay to solve specific constraints — independent deploy, independent scaling, independent teams. The price includes: network latency, distributed transactions, service governance, harder debugging, and increased deployment complexity.
So the first question is not “how to split” — it is “do I need to split at all?”
This decision framework answers three questions in order:
- To split or not — when the monolith actually hurts
- How to split — from monolith to microservices step by step
- After the split — common governance traps
1. To Split or Not: When the Monolith Actually Hurts
The Monolith’s Strengths (Do Not Underestimate)
The monolith is still optimal for:
- Teams of 1-5 people
- Unclear business domain boundaries (exploration phase)
- Pre-PMF product stage
- High data consistency requirements (e.g., core financial ledger)
Under these conditions, a monolith outperforms microservices in development speed, ops simplicity, and debugging convenience.
The Pain Threshold
When should you start thinking about splitting? Not when the codebase hits a certain line count — but when team coordination cost exceeds the simplicity benefit of the monolith. The signals:
| Indicator | Pain Signal | How to Measure |
|---|---|---|
| Deploy frequency | A feature is blocked by other modules’ issues | Weekly deploys < 3 |
| Code conflicts | Every merge touches the same files | Monthly conflicts > 5 |
| Blast radius | One module’s bug takes down the whole service | P0 incidents per quarter |
| Scaling efficiency | Only one module needs scaling, but you redeploy everything | One module > 80% utilization, others < 20% |
| Tech lock-in | Switching one module’s tech stack is an impossible refactor | Estimated refactor time > 2 weeks |
Any 3 of these signals → it is time to seriously consider decomposition.
2. How to Split: Step by Step
Step 1: Draw Boundaries by Business Subdomain (Lightweight DDD)
Do NOT split by technical layer (“extract the Controller”, “extract the Model”). Split by business capability.
Example: an e-commerce system
User domain: registration, login, permissions, addresses
Product domain: catalog, inventory, categories, search
Order domain: checkout, payment, refund, shipping
Content domain: descriptions, reviews, images, videos
Each domain is a potential microservice. The judgment rule: changes in one domain do not require synchronous knowledge from other domains — cross-domain information flows through events.
Step 2: Identify Split Candidates
Not all domains have equal split priority. Rank by two dimensions:
- Change frequency: how often this domain’s logic changes
- Resource consumption: CPU/memory/storage demand
High change + High resource → split first (search, recommendation)
High change + Low resource → split second (user, permissions)
Low change + High resource → split on demand (logs, reports)
Low change + Low resource → keep in monolith last (config, lookups)
Step 3: Start with the Easiest Module to Extract
Do not attempt a one-shot N-service decomposition. Use the Strangler Fig Pattern:
- Identify one module that can run independently
- Build a separate API layer for it (new traffic goes to new API, old traffic continues via monolith)
- Gradually migrate callers to the new API
- Once migration is complete, remove the module from the monolith
- Repeat
One service at a time, validate each: independent deploy, independent test, independent scaling.
3. After the Split: Four Common Governance Traps
Trap 1: Distributed Transactions
The most insidious microservice pitfall. What was a single database transaction in the monolith is now distributed.
Mitigation:
- Prefer eventual consistency over strong consistency
- When unavoidable: use the Saga pattern (choreography or orchestration)
- Avoid cross-service transactions entirely — redesign boundaries so the transaction falls within one service
Trap 2: Deep Call Chains
A → B → C → D → E
Each hop multiplies latency. Failure probability rises exponentially with chain depth.
Mitigation:
- Keep chain depth ≤ 3
- Beyond 3, use async events or data duplication
- Every synchronous call must have a timeout and circuit breaker
Trap 3: Shared Database
“Split the services, deal with the database later” — the most common half-baked state. Services are decomposed, but they all read from the same database.
Mitigation:
- Each service owns its database (or its own schema)
- Cross-service data exchange goes through APIs, never through the database
- For cross-service queries, use caching or CQRS
Trap 4: Over-Decomposition
A 10-person team maintaining 20 microservices — each person handling 2 services, and each feature change touches 3-5 services.
Diagnosis: if one feature change requires modifying more than 3 services, you have over-decomposed. Merge services until the “feature-change radius ≤ 2 services.”
4. Recommended Roadmap
| Phase | Goal | Duration | Output |
|---|---|---|---|
| Phase 0 | Modularize inside the monolith | 1-2 months | Clear module boundaries, interface definitions |
| Phase 1 | Extract 1-2 high-value services | 2-3 months | Independent deploy and scaling |
| Phase 2 | Extract 3-5 core services | 3-6 months | Full service governance |
| Phase 3 | Evolve on demand | Ongoing | Business-driven, never split for its own sake |
Each phase targets “the most painful constraint right now” — not a service count target.
Summary
| Question | Answer |
|---|---|
| When to split | 3+ pain signals from the threshold table |
| What to split by | Business subdomain (lightweight DDD), not technical layer |
| Where to start | High-change + high-resource modules |
| How to split | Strangler Fig, one at a time, validate each |
| How fine is enough | Feature-change radius ≤ 2 services |
| Biggest trap | Services split but the database stayed shared |
Microservice maturity is not measured by how many services you have — it is measured by whether you can independently deploy, test, and scale any single service without materially impacting the business.
Related reading
- Monolith to Microservices Migration: 6 Proven Strategies and a Practical Roadmap — from splitting decisions to safe cutover: 6 battle-tested migration strategies
- REST vs GraphQL vs gRPC — protocol selection for inter-service communication
- OpenAPI Best Practices — API contract standardization for microservices
Need an architecture review, decomposition plan, or migration roadmap? Contact us — tell us about your system and pain points, feasibility within 24 hours.
FAQ
How fine should a microservice be?
There is no universal answer, but a rule of thumb: a service is fine enough if one person can independently understand, develop, and deploy it. If splitting further means a single feature change touches N services, you have gone too far. The right boundary is "business capability" — each service corresponds to a complete business subdomain, not a database table or a single CRUD operation.
Should each service have its own database?
Start with logical isolation, not physical separation. Use schemas or database namespaces so that services access each other only via APIs, never by reading tables directly. When a service data volume or query pressure warrants independent scaling, promote it to a separate database instance. Logical isolation costs far less to migrate than splitting a shared database across multiple instances.
Synchronous or asynchronous for inter-service communication?
Principle: queries go synchronous (REST/gRPC), commands go asynchronous (message queue). Sync calls are fine for real-time lookups but introduce call-chain dependencies and cascade failures. Async calls decouple timing dependencies. In practice, most service-to-service communication should be async; only queries where the caller must block for the result should be sync.
When should you NOT decompose into microservices?
Three cases: ① Team < 5 people — ops overhead will crush a small team; a modular monolith is better. ② Unclear business domain boundaries — if you cannot delineate business subdomains clearly, your service boundaries will keep shifting. ③ Pre-PMF stage — rapid iteration is the priority, and microservice deploy/debug overhead will slow you down.
This article comes from AI Enable Harness front-line delivery practice. Need a similar system or optimization service?
Subscribe to Updates
Get notified when new articles are published. No spam, occasional updates only.
Subscribe →