Service Decomposition Review: When a New Microservice Creates a Worse Database Problem
A service split that leaves the database boundary intact is not decomposition; it is a distributed lock manager with better branding.
Situation
Most service decomposition proposals start with a reasonable pressure: one codebase has become too large for one team to change safely. Deployments queue behind unrelated work. Incidents require people who understand half the company. A single table has accumulated columns for every workflow that ever touched it. The proposed answer is familiar: extract a capability into its own microservice.
That answer can be correct. But the first review question should not be “Can this logic run behind an API?” It should be “Can this service own the state required to make its decisions?”
When the answer is no, the new service often makes the database problem worse. The code boundary moves. The data boundary does not. The organization now pays the coordination cost of distributed systems while still depending on the same shared schema, transactions, migrations, and operational blast radius.
The Problem
A common extraction looks clean on a diagram. The order service owns order workflows. The billing service owns payment state. The fulfillment service owns shipping decisions. The API calls are explicit. The repositories are separate. Each team gets a deployable unit.
Then production shows the real architecture.
The billing service still reads orders.status because pricing depends on fulfillment state. Fulfillment still joins against customers.plan_tier because delivery promises depend on account status. The order service still updates billing columns during checkout because the old transaction was the only thing preventing double submission. Every “temporary” shared query becomes part of the contract.
The result is a system with three operational failure modes:
- Schema coupling survives the split. A column rename is now a multi-service release, not an internal refactor.
- Transactions become implicit protocols. What used to be one database transaction becomes retries, polling, reconciliation, and compensating writes.
- Ownership becomes ambiguous. When a row is wrong, the team that owns the service may not own the table, and the team that owns the table may not own the user-facing failure.
The core question is therefore simple: does the proposed microservice reduce coordination around state, or does it turn one database dependency into many distributed dependencies?
Review the Data Boundary First
A service decomposition review should begin with data ownership, not HTTP endpoints. The service boundary is only credible when the service can enforce its own invariants without reaching into another service’s tables.
flowchart TD
A[decomposition proposal — new billing service] --> B[review state ownership]
B --> C{can billing own payment state}
C -->|yes| D[private billing schema — published events]
C -->|no| E[shared order database — hidden coupling]
E --> F[cross service joins — schema release coordination]
E --> G[split transactions — retries and reconciliation]
D --> H[explicit contract — API and event versioning]
H --> I[smaller blast radius — owned migrations]
The useful review is not anti-microservice. It is anti-pretend-boundary. A database table can be shared safely for a short migration window, but it should not be the steady-state integration mechanism between services.
A practical decomposition review should ask five questions.
Who owns each invariant?
If billing must guarantee “an order is charged at most once,” billing needs authoritative state for charge attempts, idempotency keys, and settlement status. If that invariant depends on reading and updating order rows owned elsewhere, the boundary is weak.
What data is copied, and why is it allowed to be stale?
Microservices often require duplication. That is not a flaw by itself. The flaw is duplicating data without naming the freshness requirement. A shipping service may keep a local projection of customer address data. It must know whether a five-minute delay is acceptable and what happens when the address changes after label creation.
Which operations still need atomicity?
If the extraction depends on atomic updates across two databases, the design has not finished. Either keep the operation together, redesign the invariant, or introduce a workflow pattern such as saga orchestration with explicit compensation.
What is the migration path off shared reads?
A service that starts by reading legacy tables should have an exit plan: backfill local state, dual-write only through controlled migration code, compare results, switch reads, and remove the old query. Without removal criteria, the shared read becomes permanent.
How will failures be repaired?
Once state crosses service boundaries, correctness depends on replay, reconciliation, idempotency, and observability. The review should include repair commands and dashboards, not only happy-path API contracts.
In Practice
Context. Martin Fowler’s published microservices guidance emphasizes decentralized data management: each service manages its own database, either different instances of the same technology or different storage technologies. The documented pattern is not “every service gets an endpoint.” It is that services own both behavior and persistence boundaries: https://martinfowler.com/articles/microservices.html
Action. Apply that pattern as a review constraint. If a proposed service cannot own the data required for its core decisions, classify the work as modularization or strangler migration, not completed service decomposition. Keep the label honest because the operational obligations are different.
Result. The team avoids the most expensive middle state: separately deployed services with one shared relational core. Shared databases preserve compile-time convenience but remove local reasoning. A query that looked harmless becomes a release dependency, an index dependency, and sometimes an incident dependency.
Learning. The documented microservice pattern is about independent change. Independent deployment without independent data ownership is only partial independence.
A second public pattern comes from Amazon’s guidance on the saga pattern for distributed transactions. AWS describes saga as a way to coordinate a sequence of local transactions, where each step publishes events or triggers the next action, and failures require compensating transactions: https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/saga.html
Context. The database transaction that used to protect a checkout flow does not survive a naive split into order, payment, and fulfillment services.
Action. Replace the old atomic assumption with an explicit workflow. Each service commits locally. The workflow records progress. Retry behavior is idempotent. Compensation is designed before launch.
Result. The system gains a visible failure model. Instead of an invisible half-committed business process spread across tables, operators can see which step failed, retry it, or compensate it.
Learning. Distributed consistency is an architecture, not an implementation detail. If the decomposition review cannot explain compensation, the split is premature.
PostgreSQL’s behavior gives a more concrete database lesson. A single relational database can enforce foreign keys, unique constraints, transactions, and isolation inside its boundary. Once those tables move behind separate services and separate databases, those guarantees no longer exist as database guarantees. They must be rebuilt at the application and workflow layer.
Context. A monolith may have a messy schema but still rely on real transactional semantics.
Action. Identify which constraints are currently enforced by the database before extracting the service. Unique indexes, foreign keys, check constraints, and transaction scopes are part of the architecture.
Result. The review surfaces hidden correctness requirements that were previously invisible because the database enforced them.
Learning. Do not decompose code until you have inventoried the constraints the database is silently carrying.
Where It Breaks
| Failure mode | Why it happens | Better response |
|---|---|---|
| Shared database after extraction | Service owns code but not state | Treat as migration phase with removal date |
| Cross-service joins | New service needs old read model | Build local projection with named staleness |
| Distributed transaction pressure | Old invariant crossed the new boundary | Keep boundary together or use saga workflow |
| Duplicate ownership | Multiple services update same row | Assign one writer and publish changes |
| Slow migrations | Schema changes require all services | Version data contracts and remove direct reads |
| Incident ambiguity | State and behavior have different owners | Put ownership in runbooks and alerts |
The table is intentionally blunt because this is where many designs fail. The hard part is not extracting code. The hard part is deciding which invariants deserve to stay together.
Sometimes the right answer is not a microservice. A modular monolith with clear internal boundaries may solve the deployment and ownership problem without introducing distributed state. Sometimes the right answer is a strangler pattern: place a new API in front of the legacy behavior, migrate one capability at a time, and retire shared database access gradually. Sometimes the right answer is a real service with private persistence, events, replay, and reconciliation.
The review should force the proposal to name which one it is.
What to Do Next
- Problem: The proposed microservice still depends on another service’s tables for core decisions.
- Solution: Redraw the boundary around state ownership, not repository structure or API shape.
- Proof: Inventory current database constraints, transaction scopes, shared reads, shared writes, and operational repair paths before approving the split.
- Action: Approve the service only when shared database access has a migration plan, an owner, observability, and a removal condition.