In brief: Microservices are an operating model and failure model, not a reward for having a large codebase.
The problem
Teams often split by technical layer, then discover every request crosses services and every release needs coordination. Distributed transactions, network failures and duplicated platform work arrive before independent scaling or ownership creates value.
Why the simple answer is not enough
Codebase size alone is not a reason to split. High change coupling, distinct availability needs, regulatory isolation or independently scalable workload are stronger evidence.
A modular monolith provides fast local transactions while revealing whether teams can respect ownership. If they cannot maintain boundaries in-process, a network will not fix the design.
System flow
ownership and invariants→Modular monolith
enforced boundaries→Extraction seam
contract and data→Service
independent operation
Architecture decisions
Split by business capability
Keep rules and data that preserve one invariant together. Avoid separate user-service, validation-service or database-service layers.
Choose synchronous calls sparingly
Use them only when the caller truly needs an immediate result; publish facts for independent downstream reactions.
Own the operational lifecycle
Extraction includes on-call, dashboards, deployment, security and capacity, not just moving code to another repository.
Going deeper
Data ownership is the hard boundary
A service may expose read models, events or APIs, but other services must not write its tables. Temporary replication needs an owner and removal date.
The strangler needs reconciliation
During migration, compare old and new results, route a small cohort and preserve a fast return path until data parity is proven.
Implementation path
- Map business capabilities, invariants and change ownership.
- Enforce module APIs and dependency rules inside the monolith.
- Extract one capability with a contract, data migration and rollback path.
Technical example: Dependency rule before extraction
module Orders exposes OrdersApi
module Billing exposes BillingApi
Orders may call BillingApi
Orders must not import BillingRepository
architectureTest.assertNoInternalDependencyLeaks()Automated architecture tests turn the intended module graph into a build-time constraint.
Failure modes to design for
- A shared database lets services bypass their contracts.
- A chatty boundary creates a distributed N+1 problem.
- Two teams believe they own the same invariant.
What to observe
| Signal | Why it matters |
|---|---|
| Cross-module change frequency | Shows boundaries that still require coordinated releases. |
| Remote calls per journey | Exposes chatty decomposition and added latency. |
| Deploy and incident independence | Tests whether extraction produced real autonomy. |
How to validate the design
- Block internal module imports in CI.
- Add latency and failure to the proposed remote boundary before extraction.
- Run old and new paths for the same inputs and reconcile outcomes.
Safe rollout plan
Start with architecture tests and one module that already has clear ownership. Extract read paths, then commands, then data. Keep a per-tenant routing flag and remove the legacy path only after operational independence is demonstrated.
Production checklist
- Each service owns its data and business decisions.
- Cross-service calls have latency, timeout and failure budgets.
- The platform can deploy, observe and secure every service consistently.
Takeaway
A good service boundary reduces coordination; a bad one merely moves function calls onto the network.
