Stackbook Logo
distribution-communicationestablished · medium operational burden

API Composition

Also known as: api-aggregation, query-composition

Intent

Answer a query spanning several services by calling each and joining the results in memory.

Problem

Data owned by separate services cannot be joined with a database query. Clients needing a combined view would otherwise make many calls and stitch the data themselves.

Forces

  • •Each service owns its data store
  • •Cross-service joins are impossible in SQL
  • •Latency is the sum or max of downstream calls
  • •Partial failures are likely

Solution

Overview

API composition is the simplest way to implement queries across service boundaries. A composer calls the services that own each piece of data and joins the answers itself. It trades storage complexity for runtime coupling.

How it works

Take an order details page needing the order, its customer and shipment status:

GET /order-details/42
  composer
    |-- orders.get(42)          -> order
    |-- customers.get(order.cid) -> customer   (needs order first)
    |-- shipping.byOrder(42)    -> shipment    (parallel with customer)
  merge -> single response

Independent calls run in parallel, so latency approximates the slowest chain, not the sum. Each call has a timeout and a fallback: if shipping times out, return the order with shipment marked unavailable. Results of slow-changing data, such as customer profiles, can be cached briefly.

Pitfalls

  • Unbounded fan-out. Cap concurrency and batch lookups instead of calling a service once per row.
  • No timeouts or circuit breakers. The composer inherits the worst downstream behaviour.
  • Silent partial results. Signal degraded responses explicitly so clients do not mistake them for complete data.
  • Joining large datasets in memory. Beyond thousands of rows, use pagination or a CQRS-style read model.
  • Over-fetching entire resources when only a few fields are needed.

Key takeaways

  • Call owners in parallel, join in memory, return one response.
  • Budget timeouts and define partial-failure behaviour up front.
  • Best for small, fresh, low-volume cross-service queries.
  • Move to a read model when joins get large or hot.
  • Measure it in production: track latency, error rates and volume for this pattern, and review the numbers regularly so that limits and timeouts stay matched to real traffic.

✓ When to Use

  • •Moderate-sized joins across a few services
  • •Queries that are infrequent or latency-tolerant
  • •Data must be fresh rather than eventually consistent

✗ When Not to Use

  • •Large result sets requiring in-memory joins of millions of rows
  • •High-traffic queries better served by a read model
  • •Joins across many services in one request

Pros

  • +Simple to build and reason about
  • +Always reads current data
  • +No extra data store

Cons

  • −Latency bound by slowest dependency
  • −Availability is product of dependencies
  • −In-memory joins do not scale for big data

Cost Profile

Infrastructure

Low - no new storage

Operational

Medium - tuning timeouts and fallbacks

Cognitive

Low - easy to understand

Failure Modes

  • One slow service stalls the whole response

  • Partial failure returns misleading incomplete data

  • Fan-out multiplies load on downstream services

Real-World Examples

Alternatives

    Related Patterns

    • circuit-breaker
    • api-gateway

    Competency Domains

    distribution communicationdata state