Lesson 68 of 129
Database per Service
Database per service is the pattern in which each microservice owns its persistent data and no other service accesses that data directly. Other services obtain or change the data only through the owning service's API or through events it publishes. The data may live in a separate database server, a separate schema, or separate tables with separate credentials. What matters is that access is exclusive to the owner.
Why ownership matters
Independent deployability requires that a service can change its internal structure without coordinating with others. A table shared by two services is a public interface: renaming a column, changing a type, or adding a constraint can break the other service and forces synchronized releases. Owning the data lets a service:
- Change its schema freely behind a stable API (see API versioning and contract testing).
- Enforce its own invariants, since all writes pass through its code.
- Scale and tune its storage independently.
- Fail independently: an overloaded database of one service does not slow the others.
flowchart LR
O["Order Service"] --> ODB[("Orders DB")]
C["Catalog Service"] --> CDB[("Catalog DB")]
I["Inventory Service"] --> IDB[("Inventory DB")]
O -->|"API call"| C
O -->|"events"| IPolyglot persistence
Since each service owns its store, each can use the storage type that matches its access patterns. This is polyglot persistence.
| Service | Access pattern | Possible store |
|---|---|---|
| Orders | Transactions, relational integrity | PostgreSQL, MySQL |
| Product catalog | Flexible attributes, document reads | MongoDB, document store |
| Sessions, carts | Key lookup, TTL | Redis, DynamoDB |
| Search | Full-text queries | Elasticsearch (see inverted index and full-text search) |
| Metrics | Append-only time series | Time-series database |
The cost is operational: each technology needs expertise, backup, monitoring, and upgrades. Limit the number of technologies to those with a clear benefit.
Levels of isolation
| Level | Description | Isolation |
|---|---|---|
| Private tables in shared server | Same database instance, separate tables, separate credentials per service | Low: shared resources, noisy neighbors, shared failure |
| Schema per service | Separate schemas or logical databases on one server | Medium |
| Database server per service | Separate instances | High, more cost |
Private tables or schemas are a pragmatic starting point. Enforce the boundary with distinct database users that can access only their own schema, otherwise the boundary erodes.
The shared database anti-pattern
Multiple services reading and writing the same tables gives up the benefits above:
- Schema changes require coordination among all services.
- Any service can violate another's invariants by writing directly.
- Lock contention and load from one service affect the others.
- Services cannot pick different storage technologies.
- The system becomes a distributed monolith: separately deployed but coupled through data.
A shared database is sometimes used temporarily during migration from a monolith, then split.
What is lost and how to compensate
Removing the shared database removes local joins and ACID transactions across entities that now live in different services.
Queries across services.
- API composition: call each service and join in memory (see BFF and API composition).
- Replicated data via events: a service keeps a local read-only copy of the fields it needs from another service, updated by that service's events. This trades freshness for autonomy.
- CQRS views: a dedicated read model built from events of several services (see event sourcing and CQRS).
See eventual consistency and cross-service queries.
Transactions across services. Use a saga: a sequence of local transactions with compensating actions, coordinated by events or an orchestrator. Distributed two-phase commit is possible only if all stores support a distributed transaction protocol (for example, XA), and it reduces availability because participants hold locks while waiting for the coordinator (see distributed transactions).
Reporting and analytics. Do not query operational databases of many services. Stream changes into a data warehouse or lake via events or change data capture (see transactional outbox and CDC).
Referential integrity. Foreign keys cannot span services. An order stores the customer ID, but the database cannot verify that it exists. Validate at write time by API call or local replica, and design for references that become dangling (for example, a deleted customer) by handling absent data.
Example: Order Service stores customer_id = 7 for an order. To display the customer name, it calls Customer Service (GET /customers/7), or reads customer_name from its own replica table filled by CustomerUpdated events. If customer 7 is later deleted, the stored ID is dangling. The order view then shows a placeholder or the name stored at order time, since the database raises no foreign-key error.
Data duplication
Replicated fields and read models mean the same fact is stored in several places. Rules:
- Exactly one service is the system of record for each fact and is the only writer.
- Copies are derived, read-only, and rebuildable from the source.
- Copies may be stale. Design the user experience and logic for that.
Interview questions
- Why not let services share one database if they use different tables? Tables are still a coupling point: shared credentials allow cross-writes, load and locks interact, and schema changes become coordination problems. Enforced ownership keeps each service's schema private.
- How do you join data owned by two services? Compose in code through their APIs, keep a replica of the needed fields updated by events, or build a read model that combines both.
- How do you keep data consistent across services without distributed transactions? Use sagas with compensating actions, and publish events reliably through the outbox pattern.
- Does database per service require separate database servers? No. It requires exclusive access. Schema-level separation with restricted credentials satisfies it, with weaker isolation of resources.
