Database-per-Service Pattern and Data Ownership in Microservices
The principle I care about
The most important part of database-per-service is not:
every microservice must buy its own database server.
It is:
one service owns writes to its domain data.
Bad coupling:
Booking Service ----\
Payment Service -----+--> shared tables
Hotel Service -------/Any service can change another service's data without its business rules.
Better:
Booking Service -> Booking data
Payment Service -> Payment data
Vendor Service -> Vendor dataPhysical isolation options
Ownership can be implemented as:
separate schema
separate database
separate cluster
separate cloud account/projectThe correct level depends on scale, security and operations.
A small platform can keep multiple owned schemas in one managed DB cluster while application permissions prevent cross-domain writes.
Cross-service reads
Option 1: API
GET /customers/C1001Option 2: local projection
CustomerUpdated
|
Kafka
|
Booking service
|
customer_snapshot tableProjection:
@EventsHandler(CustomerUpdated)
export class CustomerSnapshotProjection {
async handle(
event: CustomerUpdated,
) {
await this.bookingDb
.customerSnapshot
.upsert({
customerId: event.customerId,
displayName: event.displayName,
version: event.version,
});
}
}Reporting
A report that needs:
booking + customer + payment + productshould not automatically execute a runtime SQL join across four service databases.
I create:
- reporting warehouse;
- analytical store;
- read projection;
- event-fed reporting DB.
This keeps transactional ownership clean.
Cross-service transaction
If booking and payment must commit atomically in one database transaction, I ask:
are these actually separate service boundaries?
Sometimes the correct answer is to keep the operation inside one aggregate/service.
When boundaries are valid but operations span them, I use Saga/outbox/idempotency rather than shared writes.
Migration from shared DB
During Strangler migration:
Phase 1: shared DB, legacy write owner
Phase 2: new service reads through ACL
Phase 3: new service becomes write owner
Phase 4: legacy receives API/event projection
Phase 5: old table access removedI avoid uncontrolled bidirectional dual writes.
Production checklist
- Name the authoritative data owner.
- Restrict cross-service write credentials.
- Design APIs/events for other services.
- Build reporting separately.
- Plan backup/recovery per domain.
- Version data contracts.
- Avoid runtime cross-service SQL.
- Revisit boundaries when ACID needs cross them constantly.
- Define migration ownership stage by stage.
FAQ
Does every service need a separate DB server?
No.
Is a shared DB always wrong?
It can be a pragmatic transition, but shared writable tables reduce independent evolution.
How do I perform joins?
For operational reads use APIs/composition/local projections. For analytics use a reporting platform.
Related architecture guides
- [Microservices Design Patterns I Have Used in Real Enterprise Projects](/articles/microservices-design-patterns-real-enterprise-projects)
- [CQRS](/articles/cqrs-command-query-responsibility-segregation-real-systems)
-
Architect's final take
Microservices become genuinely independent only when data ownership is as clear as code ownership.
Your feedback helps prioritize deeper technical content.






