HT
How Things Work
System Online

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.

How It Works

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.

1
Database per Service

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.

2
The Trade-off: No Cross-Service JOIN

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.

3
Querying: API Composition vs Read Models

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.

4
Keeping Data Consistent

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

πŸ—„οΈDatabase per Service

Each service exclusively owns its database; others access the data only via its API or events.

🧩API Composition

Answer a cross-service query by calling each owning service and joining the results in the app. Fresh, but chatty.

πŸ“–CQRS Read Model

A denormalized, query-optimized view built from events, answering cross-service reads in one fast query.

πŸ“‘Data Duplication

Services keep local copies of data they need (updated via events) rather than calling other services constantly.

πŸ”„Eventual Consistency

Replicated data and read models converge over time, not instantly β€” the norm without distributed transactions.

πŸ—‚οΈPolyglot Persistence

Each service picks the storage that fits it (SQL, document, graph) since it owns its own database.

API composition vs CQRS read model
tsx
1// ❌ You CANNOT do this across services β€” no shared database, no cross-DB JOIN:
2// SELECT o.*, c.name, p.title
3// FROM orders o JOIN customers c ... JOIN products p ... (impossible)
4
5// βœ… API COMPOSITION β€” gather from each service, join in the application:
6const order = await ordersApi.get(orderId); // orders-db
7const [customer, products] = await Promise.all([ // parallel fan-out
8 customersApi.get(order.customerId), // customers-db
9 productsApi.getMany(order.productIds), // products-db
10]);
11return { ...order, customer: customer.name, items: products.map(p => p.title) };
12
13// βœ… CQRS READ MODEL β€” a denormalized view kept current by events, read in ONE query:
14events.on("OrderPlaced", e => orderView.upsert(e)); // build the view
15events.on("CustomerRenamed", e => orderView.updateName(e)); // keep it fresh
16const view = await orderView.findById(orderId); // single fast read
πŸ’‘
Why This Matters

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

⚠A shared database across services β€” recreates tight coupling and defeats independent deployment.
⚠Synchronous API composition for every read, adding latency and partial-failure risk on hot paths.
⚠Ignoring eventual consistency β€” assuming a replicated copy or read model is instantly up to date.
⚠Trying to keep data consistent with distributed (2PC) transactions instead of events/sagas.
⚠Letting read models drift with no rebuild path, so they silently diverge from the source of truth.
Real-World Use Cases

1The Report That Needs Data From Six Services

Scenario

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.

Problem

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.

Solution

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

Scenario

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.

Problem

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.

Solution

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.