Reference Architecture: Microservice data layer
In microservices the most critical decision is data ownership: each service must own its data (database-per-service) and must not reach directly into another service's tables. This tree walks step by step through the shared-DB anti-pattern, achieving cross-service consistency without distributed transactions (saga + outbox), synchronizing data with CDC, and cross-service queries (API composition vs a CQRS read model). The goal is to keep services truly independent and separately deployable.
How do services own data: a shared DB or one per service?
Data isolation is the backbone of a microservice architecture. Do services use a single shared database (a hidden dependency), does each service have its own database (the right pattern), or is the real challenge cross-service consistency/queries? Establish the current state first.
- All services use a single shared DB
- Each service has its own DB, but operations need cross-service consistency
- Each service has its own DB, but combined queries/reports are needed
How will you keep a cross-service workflow consistent?
For an operation spanning multiple services (create order → take payment → decrement stock), a classic distributed transaction (2PC) is fragile and does not scale. Instead, split the flow into local transaction + event steps (saga) and publish events reliably (outbox). Which problem are you dealing with?
- I am trying to keep a multi-service workflow atomic (2PC?)
- I cannot make the local write and event publish consistent (dual write)
How will you serve a query that shows data from multiple services?
Once data is split across services, you cannot build a combined screen with a single join. There are two main approaches: call the relevant services' APIs at request time and merge the results (API composition), or maintain a pre-joined read model fed by events (CQRS). Query frequency and complexity determine the choice.
- Simple, infrequent join; calling a few services and merging is enough
- Complex/frequent combined query; a ready read model is needed
Recommendation: Database-per-service (split the shared DB)
Services sharing one database is a hidden and dangerous dependency: one service's schema change breaks the others, ownership of tables becomes unclear and independent deployment becomes impossible. Each service must own its data.
Recommendation: Saga pattern (avoid distributed transactions)
Do not try to make an operation spanning multiple services atomic with a single distributed transaction (2PC); 2PC holds locks and brings single-point fragility and low availability. Instead, split the flow into steps that are each a local transaction in their own service, and undo them with compensation steps on failure.
Recommendation: Transactional outbox (the dual-write trap)
When a service wants to both write to its own DB and publish an event, doing the two separately is the 'dual write' trap: if one succeeds and the other fails, the system is left inconsistent. The solution is to write the event into an outbox table in the SAME local transaction as the business data, then publish it with a separate process.
Recommendation: API composition (merge at request time)
Where combined data is needed, the caller (e.g. a gateway/BFF) calls the relevant services' APIs and merges the results in memory. It is the simplest solution for simple joins spanning few services; no extra data copy is kept.
Recommendation: CQRS read model (fed by events)
For complex and frequent combined queries, keep a pre-joined, query-optimized read model (materialized view/denormalized copy) fed by events emitted by the services. The write side stays in the services; the read side serves ready data quickly from one place.