Self-Contained Service
Context
Section titled “Context”Your services are well bounded, each owning its own data. Now they have to collaborate. The Order service, to display an order, needs the customer’s name and the product’s title — data owned by the Customer and Catalog services. The obvious move is to call those services synchronously whenever an order is rendered. It works in development, where every service is up and the network is a loopback. Then you go to production.
Problem
Section titled “Problem”The instant one service must make a synchronous call to another to do its own job, it inherits that other service’s availability — and its latency. Chain a few of these calls and the maths turns hostile: if four services each have an excellent 99.9% availability, a request that depends on all four synchronously is up only about 99.6% of the time, and any one slow dependency drags the whole chain’s latency up with it. A failure or a slowdown anywhere in the chain becomes a failure everywhere upstream. You have built services that deploy independently but cannot run independently — a distributed system that is more fragile than the monolith it replaced.
So the forces are: a service needs data that another service owns, but it must not surrender its own availability and responsiveness to that other service every time it serves a request.
Solution
Section titled “Solution”A self-contained service handles each request entirely on its own, without making a synchronous call to any other service in the request path. It achieves this by keeping a local replica of the data it needs from elsewhere — just the slice it actually uses — and keeping that replica fresh asynchronously, by subscribing to the events the owning services publish.
The owning service stays the source of truth. When the Customer service changes a name, it publishes a CustomerUpdated event; the Order service consumes it and updates its small local copy of customer names. Later, when the Order service serves a request, the data it needs is already sitting in its own database. No call goes out across the network on the hot path, so the request succeeds even if the Customer service is down for maintenance.
The trade is deliberate: you accept that the local replica is eventually consistent — it may lag the source by a moment — in exchange for the service being able to answer requests on its own. For the vast majority of read paths, a name that is a few seconds stale is a perfectly good trade for staying available.
flowchart TB
subgraph Chain[Synchronous chain - fragile]
direction LR
Req1[Request] --> O1[Order Service]
O1 -->|sync call| C1[Customer Service]
O1 -->|sync call| P1[Catalog Service]
note1[If Customer or Catalog is down or slow,<br/>the request fails or hangs]
end
subgraph Self[Self-contained - resilient]
direction LR
Req2[Request] --> O2[Order Service]
O2 --> R2[(Local replica:<br/>names, titles)]
C2[Customer Service] -. CustomerUpdated event .-> R2
P2[Catalog Service] -. ProductUpdated event .-> R2
note2[Request served from local replica;<br/>events update it in the background]
end Resulting context
Section titled “Resulting context”What you gain:
- Availability that does not multiply downward. The service answers requests without depending on others at runtime, so a neighbour’s outage or slowdown no longer takes it down. Availability stops being the product of every dependency’s availability.
- Lower, more predictable latency. No synchronous network hops on the hot path means no tail-latency surprises imported from a slow dependency.
- Looser runtime coupling. Services collaborate through asynchronous events rather than synchronous calls, which also lets each deploy and restart without coordinating downtime.
What it costs you:
- Eventual consistency. The local replica can lag the source of truth. This is fine for displaying a name, but unacceptable for decisions that need an authoritative, up-to-the-instant value (such as an account balance at the moment of a transfer) — those still need the owning service.
- Replicated data to manage. You now store and maintain copies of other services’ data, which means more storage, the discipline to keep the copy minimal, and care to handle out-of-order or duplicate events idempotently.
- An event backbone you must run. This pattern presumes reliable event publishing and consumption — a message broker and the operational maturity to keep it healthy — which is real infrastructure to build and own.
Related patterns
Section titled “Related patterns”- Decompose by Subdomain — clear bounded contexts make it obvious which slice of foreign data a service actually needs to replicate.
- Saga — the asynchronous, event-driven style that self-contained services rely on for cross-service writes.
- CQRS — building a local read model from events is the same mechanism that keeps a self-contained replica fresh.
| Benefit | Trade-off |
|---|---|
| Answers a request on its own, without blocking on another service | Has to replicate data from other services — eventual consistency |
| Resilient — another service failing doesn’t affect this request | Data-sync complexity goes up |
| Lower latency — no synchronous hop | The replicated data can go stale |
| Scales independently of the services it depends on | Uses more storage because of data replication |
Common mistakes
Section titled “Common mistakes”Premature self-containment — replicating everything for no real reason. Symptoms:
- A service replicates data from 10 services because it “wants to be self-contained.”
- The sync overhead ends up worse than the synchronous call it was avoiding.
Self-contained but stale — no good mechanism to keep the data in sync. Symptoms:
- A service shows an old price because of replication lag.
- A user sees stock available, then it’s gone at checkout.
💡 Real-world examples
A common pattern in large streaming systems: a recommendation service often keeps a local copy of catalog metadata rather than calling the catalog service synchronously on every request — if the catalog service fails or slows down, recommendations can still answer using the data synced earlier (possibly a bit stale). This is a general illustration of the pattern, not a claim about any specific company’s internal system.
A common pattern in ride-hailing systems: a pricing service often caches infrequently-changing data (such as zone boundaries or base rates) locally, so it can compute a fare without calling another service on every request.