name: architecting-service-boundaries description: Decides how to split (or not split) a backend into services — modular monolith by default, boundaries along business capabilities, data ownership per service, sagas over distributed transactions, strangler-pattern extraction. Use when the user asks whether to adopt microservices, how to split a monolith, where service boundaries belong, how services should share data, or how to handle transactions across services. Do not use for message-broker mechanics (handling-async-messaging) or REST endpoint design (designing-rest-apis).
Architecting Service Boundaries
When to use / when NOT to use
- Use for: monolith-vs-services decisions, drawing/reviewing service boundaries, cross-service data and transaction design, extraction plans.
- Do NOT use for: broker/queue mechanics (handling-async-messaging), endpoint design (designing-rest-apis), or infra scaling (scaling-backend-services).
Core rules
- Default to a modular monolith. One deployable with enforced internal module boundaries. Split a module out only on a proven trigger: independent scaling need, independent deploy cadence, or team-ownership friction — never "because microservices."
- Boundaries follow business capabilities, not technical layers.
- ✅
billing,catalog,fulfillment - ❌
api-service,business-logic-service,database-service
- ✅
- Each service owns its data. No other service reads its tables; integration happens through its API or published events. A shared database is one service wearing several trench coats.
- Keep synchronous call chains shallow — ~2 hops max. Request → A → B is acceptable; A → B → C → D couples four uptimes and multiplies latency. Deeper flows go asynchronous via events.
- Distributed transactions are sagas. A sequence of local transactions with explicit compensating actions for each step. Never reach for two-phase commit; design the compensation before the happy path.
- Contracts are versioned and backward compatible. Additive changes only within a version; breaking changes get a new version with a deprecation window. A consumer must never be forced to deploy in lockstep with a provider.
- Extract with the strangler pattern. Route a slice of traffic through the new service while the monolith still serves the rest; retire monolith code only after parity is proven. Big-bang rewrites forfeit the rollback path.
- Watch for the distributed monolith. Services that must deploy together, share a schema, or break when one is down have all of the costs of microservices and none of the benefits — re-merge or re-draw the boundary.
Workflow
- List business capabilities and the teams that own them; sketch candidate boundaries there.
- For each candidate split, name the concrete trigger from rule 1. No trigger → stays a module.
- For each boundary: define the owned data, the exposed contract, and the events published.
- Map every cross-boundary write flow as a saga with compensations (rule 5).
- Plan extraction via strangler routing with a rollback switch (rule 7).
- Validate the design: for each service, confirm it can deploy alone with every other service frozen, and survive (degraded, not down) any single dependency being offline. Any "no" is a rule-8 smell — redraw before building.
Edge cases & failure modes
- Two services keep changing in the same PRs → boundary is wrong; merge them or move the shared concept to one owner.
- A service needs another's data constantly → replicate via events into a local read model instead of chatty sync calls.
- Reporting needs to join across services → dedicated analytics store fed by events; never grant cross-service table access.
- Saga compensation impossible (e.g., email already sent) → make the step last, or design it to be semantically reversible ("correction" message).
References
Decision tables, saga and strangler sketches: see references/patterns.md.