Strategies to Share Data Between Services
Splitting an application into services creates an important architectural problem: each service owns its data, but other services frequently need that data to complete their work.
An Order Service may need product prices. A Shipping Service may need delivery addresses. A Recommendation Service may need information about completed purchases. A Fraud Service may need payment activity. The simplest solution is often to call the service that owns the data, but that approach can introduce latency, runtime dependencies, cascading failures, and scalability problems.
Production systems therefore use several strategies for sharing data between services. The most common approaches are synchronous requests, asynchronous requests, event-driven replication, and hybrid architectures. Each provides different guarantees around freshness, consistency, availability, latency, and complexity.
Table of Contents
- Data Ownership Comes First
- 1. Synchronous Data Sharing
- 2. Asynchronous Data Sharing
- 3. Event-Driven Data Sharing
- 4. Hybrid Data Sharing
- Freshness and Consistency
- Choosing a Data-Sharing Strategy
- Production Design Example
- Common Mistakes
- Production Checklist
- Conclusion
Data Ownership Comes First
Before choosing how services share data, ownership must be clear. In a service-oriented architecture, a service should normally be the authoritative owner of the data associated with its business capability.
Product Service -> products, prices, descriptions
Inventory Service -> stock levels, reservations
Order Service -> orders, order items
Payment Service -> payments, refunds
Shipping Service -> shipments, tracking
If the Product Service owns product information, another service should not directly modify the Product Service database. Instead, it obtains the required information through an API, message, event, or locally replicated representation.
This distinction is important because sharing data does not necessarily mean sharing a database. Services can exchange or replicate information while preserving clear ownership boundaries.
Database ownership and data boundaries are covered in more detail in Managing Data Across Multiple Services.
1. Synchronous Data Sharing
Synchronous communication is the most direct strategy. A service requests data from another service and waits for the response.
Order Service
|
| GET /products/42
v
Product Service
|
| product data
v
Order Service
REST and gRPC are common implementations of this pattern.
Example
Suppose the Order Service needs the current product price while creating an order:
import httpx
async def get_product(product_id: int) -> dict:
async with httpx.AsyncClient(timeout=1.0) as client:
response = await client.get(
f"http://product-service/products/{product_id}"
)
response.raise_for_status()
return response.json()
async def create_order(product_id: int, quantity: int):
product = await get_product(product_id)
return {
"product_id": product_id,
"quantity": quantity,
"unit_price": product["price"],
"total": product["price"] * quantity,
}
The main advantage is freshness. Assuming the Product Service itself contains current information, the Order Service receives the latest product state at request time.
The cost is a runtime dependency. Creating an order now depends on the Product Service being reachable and sufficiently fast.
Client
|
v
Order Service
|
+--> Product Service
|
+--> Inventory Service
|
+--> Promotion Service
|
+--> Customer Service
As the number of synchronous dependencies grows, latency accumulates and availability decreases. One slow dependency can delay the entire operation, while one unavailable dependency may cause the request to fail.
When Synchronous Sharing Works Well
Synchronous requests are appropriate when the caller needs fresh data before it can continue, especially for small request chains and operations where stale information would produce an incorrect business decision.
Examples include checking current permissions, retrieving a configuration required immediately, or performing an authoritative validation.
Timeouts, retries, circuit breakers, and bounded concurrency become essential once synchronous service calls are placed on critical request paths. See Timeouts, Retries, and Exponential Backoff for the failure-handling patterns behind these calls.
2. Asynchronous Data Sharing
Not every consumer needs data immediately. When work can happen later, services can communicate asynchronously through a queue or broker.
Order Service
|
| message
v
Message Queue
|
v
Shipping Service
The producer sends a message and continues without waiting for the consumer to complete its work.
Example
After an order is created, the Order Service could enqueue a request for shipment preparation:
{
"type": "prepare_shipment",
"order_id": "ORD-98142",
"customer_id": "CUS-492",
"items": [
{
"product_id": 42,
"quantity": 2
}
]
}
The Shipping Service processes the message independently.
This removes the Shipping Service from the synchronous order-creation path:
Without queue:
Client -> Order -> Shipping -> response
|
+ failure affects request
With queue:
Client -> Order -> Queue -> response
|
+----> Shipping later
The queue acts as a buffer between production and consumption rates. If the Shipping Service temporarily processes 500 messages per second while the Order Service produces 800, the backlog can absorb the difference rather than immediately rejecting requests.
The Main Trade-Off
Asynchronous communication improves decoupling and resilience, but introduces delayed processing and additional operational concerns.
Consumers must handle duplicate messages, retries, poison messages, ordering constraints, and partial failures. Queues must also be monitored for backlog growth because a healthy producer can hide an unhealthy consumer for some time.
Asynchronous messaging works particularly well for background processing, notifications, report generation, media processing, fulfillment workflows, and other operations that do not need to complete before the original request returns.
3. Event-Driven Data Sharing
Event-driven architecture takes asynchronous communication further. Instead of telling another service what to do, a service publishes a fact about something that already happened.
Order Service
|
| OrderCreated
v
Event Bus
/ | \
v v v
Email Analytics Shipping
The Order Service does not need to know which systems are interested in the event. Multiple consumers can independently subscribe to it.
Example
{
"event_id": "evt_89123",
"event_type": "OrderCreated",
"occurred_at": "2026-08-31T20:14:22Z",
"order": {
"id": "ORD-98142",
"customer_id": "CUS-492",
"total": 129.90
}
}
The event may be consumed by several services:
+--> Analytics Service
|
OrderCreated -------+--> Notification Service
|
+--> Loyalty Service
|
+--> Fraud Service
New consumers can often be added without modifying the producer. This property makes event-driven architectures useful when many systems need information about the same business changes.
Building Local Data Projections
Events can also solve a more interesting data-sharing problem: avoiding repeated synchronous calls by maintaining local copies of data.
Consider an Order Service that frequently needs product names and prices. Calling the Product Service for every request creates a dependency:
Order Service ---> Product Service ---> Product DB
Instead, the Product Service can publish changes:
Product Service
|
| ProductCreated
| ProductPriceChanged
| ProductDeleted
v
Event Bus
|
v
Order Service
|
v
Local Product Projection
The Order Service can maintain a small local table containing only the product fields it needs:
CREATE TABLE product_projection (
product_id BIGINT PRIMARY KEY,
name TEXT NOT NULL,
price NUMERIC(12, 2) NOT NULL,
version BIGINT NOT NULL,
updated_at TIMESTAMP NOT NULL
);
Reads become local and fast:
SELECT product_id, name, price
FROM product_projection
WHERE product_id = 42;
This eliminates a network hop and allows the consumer to continue reading product information even when the Product Service is temporarily unavailable.
The price is eventual consistency. There is a period between the Product Service committing an update and the Order Service consuming the corresponding event during which the local copy is stale.
4. Hybrid Data Sharing
Large production systems rarely use only one strategy. Different data has different requirements, so synchronous requests, messages, events, caches, and local projections are commonly combined.
Consider a ride-sharing platform:
+----------------+
| Ride Service |
+-------+--------+
|
+-----------------+------------------+
| | |
v v v
User Service Pricing Service Event Bus
synchronous synchronous |
|
+---------+---------+
| |
v v
Analytics Notification
Service Service
The Ride Service may synchronously retrieve information required to accept a ride, while publishing events for analytics, notifications, billing pipelines, or machine-learning systems that do not belong on the critical request path.
A particularly useful hybrid strategy is local data first with authoritative fallback.
async def get_product(product_id: int):
product = await product_repository.find(product_id)
if product and not product.is_too_old():
return product
product = await product_client.get(product_id)
await product_repository.upsert(product)
return product
The common path uses local data, while a synchronous request is reserved for missing or excessively stale information. This can substantially reduce dependency traffic while retaining a way to retrieve authoritative state.
Freshness and Consistency
The central trade-off in service data sharing is often freshness versus independence.
| Strategy | Data Freshness | Runtime Coupling | Read Latency | Complexity |
|---|---|---|---|---|
| Synchronous API | High | High | Network-dependent | Low–Medium |
| Async Message | Delayed | Low | Not request-bound | Medium |
| Event + Local Projection | Eventually consistent | Low | Low | High |
| Hybrid | Configurable | Medium | Usually low | High |
The important question is not simply whether stale data is possible. The useful question is:
How stale can this particular data safely become?
A product description being several seconds behind may be harmless. Inventory availability during checkout may require much tighter guarantees. Authorization changes may require immediate enforcement. Analytics data may tolerate minutes of delay.
Consistency requirements should therefore be defined per business operation rather than globally for an entire architecture.
Choosing a Data-Sharing Strategy
A practical decision starts with the business requirement rather than the communication technology.
| Requirement | Typical Strategy |
|---|---|
| Latest authoritative value required immediately | Synchronous API |
| Work can happen later | Asynchronous message |
| Many consumers react to the same change | Domain event |
| Very frequent reads of another service's data | Local projection |
| Consumer must operate during producer outages | Local projection or cached data |
| Freshness matters but most reads can tolerate some staleness | Hybrid local + synchronous fallback |
There is also a useful architectural signal: if Service A calls Service B on almost every request simply to retrieve the same slowly changing data, the architecture may benefit from moving that data closer to Service A.
Conversely, replicating every piece of data into every service creates unnecessary synchronization complexity. Local projections should contain only the information needed for the consumer's responsibilities.
Production Design Example
Consider an e-commerce checkout composed of Product, Inventory, Order, Payment, and Notification services.
Checkout
|
v
+--------------+
| Order Service|
+------+-------+
|
synchronous|
+--------+---------+
| |
v v
Inventory Service Payment Service
|
|
reserve stock
|
v
Order saved
|
| OrderCreated
v
Event Bus
/ | \
v v v
Notification Analytics Fulfillment
Several strategies coexist because the operations have different requirements.
Inventory reservation is synchronous because checkout should not confirm an order that cannot be fulfilled. Payment authorization is synchronous when successful payment is required before confirmation.
After the critical transaction succeeds, the Order Service publishes an OrderCreated event. Notifications, analytics, and downstream fulfillment can process that information asynchronously because their temporary unavailability should not prevent checkout from succeeding.
The Order Service may also maintain a local projection of basic product information used for order history. This avoids querying the Product Service every time a customer opens an old order.
The result is not a purely synchronous or purely event-driven system. It is an architecture where communication style follows the consistency and availability requirements of each operation.
Common Mistakes
Using Another Service's Database as an API
Directly querying another service's tables appears efficient because it avoids an HTTP request, but it couples the consumer to internal schemas and storage decisions.
Order Service ------+
|
Product Service ----+----> Shared Database
|
Search Service -----+
A Product Service migration can unexpectedly break Order or Search. Database schemas effectively become undocumented public APIs, making independent deployments increasingly difficult.
Building Long Synchronous Call Chains
API
|
v
Service A
|
v
Service B
|
v
Service C
|
v
Service D
Every network call adds latency and another failure point. Deep synchronous chains are particularly dangerous because failures propagate backward through the request path.
Using Events as Remote Procedure Calls
An event should normally describe something that happened:
OrderCreated
PaymentCompleted
ProductPriceChanged
Commands such as SendEmailNow or UpdateAnalyticsDatabase represent a different communication model. Mixing commands and domain events without clear semantics makes ownership and failure handling harder to reason about.
Putting Entire Database Records Into Events
Publishing every field makes consumers dependent on producer internals and increases event size. Events should expose a stable contract containing the information required to describe the business change.
Ignoring Duplicate Delivery
Message brokers and event systems commonly provide at-least-once delivery semantics. Consumers should therefore assume that the same event can arrive more than once.
async def handle_order_created(event):
if await processed_events.exists(event["event_id"]):
return
await create_shipment(event["order"]["id"])
await processed_events.add(event["event_id"])
Without idempotency, retries can create duplicate shipments, payments, emails, or database records.
Production Checklist
- Define one authoritative owner for each business entity.
- Avoid direct access to databases owned by other services.
- Use synchronous calls only when the caller actually requires an immediate answer.
- Keep synchronous dependency chains short.
- Configure explicit connection and request timeouts.
- Use asynchronous communication for work that can happen later.
- Make message and event consumers idempotent.
- Monitor consumer lag, queue depth, failure rates, and dead-letter queues.
- Version externally consumed API and event contracts carefully.
- Replicate only the fields consumers actually need.
- Define acceptable staleness for local projections.
- Provide reconciliation mechanisms when replicated state can diverge.
Conclusion
There is no universal mechanism for sharing data between services. The appropriate strategy depends on how fresh the data must be, whether the caller can wait, how much runtime coupling is acceptable, and what should happen when another service is unavailable.
Synchronous APIs provide immediate authoritative answers but introduce runtime dependencies. Asynchronous messages decouple work from request processing. Events allow multiple consumers to react independently and can maintain fast local projections. Hybrid architectures combine these approaches when different parts of a workflow require different guarantees.
The most resilient architectures avoid treating communication style as a system-wide decision. Instead, each interaction is designed according to its consistency, latency, availability, and failure requirements.
Key Takeaway: Keep ownership centralized, but move the information required for frequent reads closer to consumers when the business can tolerate eventual consistency. Use synchronous communication for decisions that genuinely require current authoritative state, and asynchronous communication for everything that does not need to block the critical path.
Comments (0)