Database Per Service
Database Per Service
One of the most consequential decisions in a microservices system is the rule that each service owns its data store exclusively. No other service may connect to that database directly. This principle — Database Per Service — is not a nice-to-have; it is the structural guarantee that allows services to evolve independently. Without it, the decoupling that microservices promise is largely fictional.
Why Shared Databases Are Dangerous
In a monolith, every module reads and writes the same schema. It feels convenient. The moment you extract services but leave a shared database, you have distributed the runtime without distributing the coupling. Now you have the worst of both worlds: the operational overhead of distributed processes plus the tight coupling of a monolith.
Concrete problems that arise immediately:
- Schema change paralysis. Renaming a column in the
orderstable requires coordinating every service that touches it. Deployments become a system-wide event instead of a per-team decision. - Unexpected load and contention. A heavy query in the reporting service acquires locks or chews CPU, degrading the order service that shares the same host.
- Uncontrolled data coupling. A service can read or mutate data it logically should never touch. Data contracts vanish; bugs become non-local.
- Security blast radius. A compromised service credential gives an attacker direct SQL access to every table in the shared instance — orders, payments, users, everything.
What the Pattern Actually Means
Database Per Service does not mandate a separate physical server for every service (though that is allowed). It mandates a separate logical isolation boundary: the data for a service is reachable only through that service's API. The isolation can be implemented at several levels:
- Separate schema / separate database on a shared database server — the minimum viable approach and common in early decompositions.
- Separate database server — full physical isolation; required when services have divergent SLA, scaling, or compliance requirements.
- Polyglot persistence — each service picks the storage technology best suited to its workload: a relational database for the order service, a document store for the catalog, Redis for the session service, a time-series database for the metrics service.
SELECT * FROM service_a_schema.orders, the pattern is broken.
Implementing the Boundary in Spring Boot
In Spring Boot 3 the cleanest way to enforce this is to give each service its own DataSource configured in its own application.yml. There is no shared bean, no shared connection pool, no shared credentials.
Order service configuration (application.yml):
Inventory service configuration (application.yml):
Notice that the order service has no InventoryItem entity and the inventory service has no Order entity. They are completely separate JPA contexts. When the order service needs to check stock, it calls the inventory service's REST API — not the database.
${ORDER_DB_USER} and ${ORDER_DB_PASSWORD} are never committed to source control. In Kubernetes they come from Secrets; in Docker Compose from an .env file. This directly limits the blast radius of a compromised service: an attacker who reads the order service's environment can only reach the order database.
Handling Data That Used to Be a JOIN
The most common objection to Database Per Service is: "But I need data from both services in one query." In a monolith you would write a SQL join. Distributed, you have three main options:
- API Composition. The calling service (or a dedicated BFF/API Gateway) fetches from both services and merges the results in code. Correct for low-volume reads.
- CQRS + Event-Driven Read Model. Services publish domain events; a separate read-service (or the same service's read side) maintains a denormalized projection optimised for query. The order service publishes
OrderPlaced; the reporting service consumes it and stores a flat row that already contains the product name it received from the inventory service via a separate event. No join needed at query time. - Shared reference data. Truly static data (country codes, currency codes) can be published to a shared read-only store or replicated via events. It is owned by one service; all others cache a read-only copy.
Data Consistency Without Shared Transactions
A single ACID transaction spanning two databases is impossible without a distributed transaction protocol (2PC), which is slow and couples services at the protocol level. Instead, microservices embrace eventual consistency via the Saga pattern:
- Each step of a multi-service operation is a local transaction in one service's database.
- If a later step fails, compensating transactions undo the earlier steps.
- The system converges to a consistent state, but not atomically.
This is a deliberate trade-off: you gain deployment independence and fault isolation; you pay with more complex failure handling. Lesson 7 of this tutorial covers distributed data and consistency in full detail.
Security Implications
Database Per Service is also a security boundary. With separate credentials and network policies (each database only reachable by its own service's pod/container), a successful exploit of one service cannot directly exfiltrate another service's data. In regulated environments (PCI-DSS, HIPAA) this isolation is often a compliance requirement, not just an architectural preference.
SELECT, INSERT, UPDATE, DELETE on the orders schema — not DROP TABLE, not superuser access. Use REVOKE and role-based grants when provisioning the schema.
Summary
Database Per Service enforces a hard access boundary: service data is reachable only through that service's published API. This enables independent deployability, technology choice per service, and a meaningful security blast-radius limit. The cost is that cross-service queries become API compositions or event-driven read models, and cross-service mutations become Sagas rather than ACID transactions. The trade-off is intentional and fundamental — embrace it early rather than retrofitting it under production pressure.