Data Management
Owning data per service — the database-per-service pattern, why cross-service JOINs disappear, and how API composition, CQRS read models, and events fill the gap.
In microservices, each service owns its own database and no one else touches it directly. This keeps services independent and lets each pick the right storage — but it fragments data, so cross-service queries and transactions can't use a simple JOIN or one ACID transaction. You compose data via APIs or pre-joined CQRS read models, and keep copies consistent with events and sagas.
Each microservice owns its data and its database; no other service may touch it directly — only through the owning service's API or events. This is what makes services truly independent: they can choose their own schema and storage technology, and deploy without coordinating database migrations with others.
The cost of that independence is that data is fragmented across many databases. You can't write a single SQL query joining orders, customers, and products when they live in three separate stores. Queries and transactions that used to be trivial in a monolith now span services.
Two patterns solve cross-service reads. API Composition calls each owning service and joins the results in the application — simple, always fresh, but more network calls and exposed to partial failure. A CQRS read model maintains a denormalized view (e.g. OrderView) updated by events, answering the query in one fast read — at the cost of eventual consistency.
Without distributed transactions, services keep each other's data eventually consistent via events: when something changes, the owner publishes an event and interested services update their own copies or read models. Writes that span services use sagas (compensating transactions) rather than a single ACID transaction.
Key Concepts
Each service exclusively owns its database; others access the data only via its API or events.
Answer a cross-service query by calling each owning service and joining the results in the app. Fresh, but chatty.
A denormalized, query-optimized view built from events, answering cross-service reads in one fast query.
Services keep local copies of data they need (updated via events) rather than calling other services constantly.
Replicated data and read models converge over time, not instantly — the norm without distributed transactions.
Each service picks the storage that fits it (SQL, document, graph) since it owns its own database.
1// ❌ You CANNOT do this across services — no shared database, no cross-DB JOIN:2// SELECT o.*, c.name, p.title3// FROM orders o JOIN customers c ... JOIN products p ... (impossible)45// ✅ API COMPOSITION — gather from each service, join in the application:6const order = await ordersApi.get(orderId); // orders-db7const [customer, products] = await Promise.all([ // parallel fan-out8 customersApi.get(order.customerId), // customers-db9 productsApi.getMany(order.productIds), // products-db10]);11return { ...order, customer: customer.name, items: products.map(p => p.title) };1213// ✅ CQRS READ MODEL — a denormalized view kept current by events, read in ONE query:14events.on("OrderPlaced", e => orderView.upsert(e)); // build the view15events.on("CustomerRenamed", e => orderView.updateName(e)); // keep it fresh16const view = await orderView.findById(orderId); // single fast read
Data ownership is the hardest part of microservices and a deep design-interview topic. Decisions about database-per-service, how to query across boundaries, and how to keep data consistent (events, sagas, read models) determine whether your services are truly decoupled or a distributed monolith chained together by a shared database.
Common Pitfalls
1The Report That Needs Data From Six Services
An admin dashboard must show orders enriched with customer, product, shipping, and payment details. Each lives in a different service's database, and assembling the view with six sequential API calls per row is painfully slow.
Cross-service composition at read time is chatty and slow, especially for list/reporting screens, and a single slow or down service degrades or breaks the whole dashboard.
Build a CQRS read model: an OrderSummary view that subscribes to events from each service (OrderPlaced, CustomerUpdated, ShipmentCreated…) and maintains a denormalized, pre-joined record. The dashboard reads this one view in a single fast query — composition happened ahead of time via events.
Takeaway: For read-heavy cross-service views, pre-join the data into a CQRS read model fed by events rather than composing at query time. You trade eventual consistency for fast, resilient reads.
2The Shared Database That Coupled Everything
To 'reuse data', several services were pointed at the same database, reading and writing each other's tables directly. Now a schema change for one service breaks three others, and nobody can deploy independently.
A shared database recreated tight coupling at the data layer — the very thing microservices aim to avoid. Services became entangled through the schema, defeating independent deployment and ownership.
Give each service its own database and forbid direct cross-service table access. Services expose data through APIs and publish events; others keep the local copies they need, updated via those events. Schema changes become a private concern of the owning service.
Takeaway: A shared database is a microservices anti-pattern — it couples services through the schema. Database-per-service with API/event-based data sharing preserves the independence that justifies microservices in the first place.
Join the discussion
Comments
Loading comments...
Cookie preferences
We use essential cookies for sign-in and preferences. With your permission, we also use basic analytics to improve lessons.