AzteronAzteron
The Hidden Costs of Database-per-Service in Microservice Architectures
Insights

Architecture · 9 min read

The Hidden Costs of Database-per-Service in Microservice Architectures

Database-per-service is the right default for microservices — but it introduces costs that teams underestimate until they are operating the system in production.

The Default That Teams Accept Without Fully Understanding

Database-per-service is one of the foundational principles of microservice architecture. Each service owns its data exclusively. No shared tables. No direct cross-service database access. Other services get access to the data only through the service's API.

The reasoning is sound: shared databases couple services at the storage layer. A schema change in a shared table requires coordination across every service that reads from it. One service can lock tables in ways that degrade performance for others. Deployment independence — the entire point of the microservice model — is undermined when services share a data store.

So the principle is right. But adopting it without understanding what it costs produces systems that are technically correct in their architecture and operationally miserable to run.

Cross-Service Queries Become an Architectural Problem

In a monolith, joining two tables is a single SQL statement. In a microservice system with database-per-service, joining data that lives in two different services is an architectural problem with no clean solution.

The common approaches are:

API composition: the caller fetches data from each service separately and joins it in application code. This works for simple cases. It becomes a performance problem when the volume of records is large, because you are doing in-memory joins on data you fetched over the network — with N+1 query patterns appearing naturally when joins involve related records.

Event-driven data replication: services publish events when their data changes. Other services maintain local read replicas of the data they need. The order service maintains a local copy of product and customer data it needs for queries, populated by consuming events from the product and customer services.

This is a genuinely good pattern at scale. It also adds significant operational surface: you must guarantee event delivery, handle schema evolution in the events, monitor replication lag, and ensure that local replicas stay consistent with the source of truth. None of this is free.

CQRS with a shared read model: a separate read service subscribes to events from multiple services and builds a denormalized read model optimised for specific query patterns. This works well for reporting and search. It does not work for transactional reads that need to reflect writes the user just made.

None of these options are as simple as a JOIN. The transition from monolith to microservices includes accepting this complexity and designing deliberately for it.

Distributed Transactions Are the Most Dangerous Trap

The monolith has access to database transactions. When an order is placed, inventory is decremented and the order is created in a single atomic operation. If anything fails, everything rolls back.

With database-per-service, that transaction is now distributed across two services with independent databases. There is no single transaction coordinator. ACID is no longer available as a primitive.

The Saga pattern is the standard replacement. A saga coordinates a sequence of local transactions, each in a different service, with compensating transactions to undo completed steps if a later step fails.

In the order example: the order saga creates the order (local transaction in order service), then reserves inventory (local transaction in inventory service), then charges payment (local transaction in payment service). If the payment fails, the saga runs compensating transactions to release the inventory reservation and cancel the order.

This works, but it changes the failure model significantly. Compensating transactions can fail. The system can end up in partial states during failures. Debugging a saga that failed mid-execution requires correlating state across multiple services and understanding which compensating transactions ran and which did not.

The Saga pattern is not complicated to understand. It is complicated to implement correctly, test exhaustively, and operate confidently in production — especially when failure scenarios involve compensating transactions that themselves fail.

Data Consistency Guarantees Weaken by Default

In a single database, isolation levels give you predictable consistency guarantees within a transaction. In a distributed system, those guarantees do not extend across service boundaries.

What you have instead is eventual consistency: the system will eventually reach a consistent state, but at any given point in time, reads from different services may reflect different snapshots of the world.

For many operations this is acceptable. For some it is not. Payment systems, inventory management, and financial reconciliation often have strict consistency requirements that are genuinely difficult to satisfy with eventually consistent architecture. These requirements should be identified before the service boundaries are drawn, not discovered in production when a race condition creates a double-spend or an oversell.

The Operational Cost Is Proportional to the Number of Services

Each service has its own database instance to provision, monitor, back up, patch, and tune. In a small microservice system — ten services — this is ten databases to operate. In a large system, it is more.

This is not an argument against the pattern. But it is a cost that should be explicit in the architectural decision. Database proliferation without a clear operational model creates systems where nobody has full visibility into data health across the entire platform.

The minimum operational requirements for each service database: automated backups with tested restore procedures, slow query monitoring with alerting, connection pool configuration and monitoring, index maintenance, and capacity planning. Multiply these by the number of services. The team operating the system needs capacity to handle this.

When the Pattern Is Worth the Cost

Database-per-service is worth the cost when:

Services have genuinely independent data ownership. The user service owns user profiles. The order service owns orders. These domains do not share data — they reference each other by ID, and that reference is the API boundary. In this shape of system, the costs of cross-service data access are minimal because cross-service queries are infrequent by design.

Independent scaling matters. A search index that receives read-heavy traffic should scale independently of a write-heavy order management database. Database-per-service makes this straightforward.

Teams have the operational capacity to manage distributed data. This is the constraint most often underestimated. The pattern works well for organizations with platform engineering teams that provide managed database infrastructure. It works less well for small teams where every engineer is also responsible for infrastructure.

The principle is sound. The costs are real. Treat database-per-service as a deliberate architectural choice with explicit tradeoffs — not as a rule that applies unconditionally because microservices demand it.


The systems that handle database-per-service well are ones that designed for the complexity from the start: clear data ownership boundaries, explicit consistency requirements per domain, and operational infrastructure that matches the scale of what they are running. The ones that struggle adopted the pattern without the supporting design work.