API Composition / Aggregator Pattern
Combine data across microservices: In-memory parallel fan-out aggregation, scatter-gather, timeout handling, and partial degradation.
Scatter-Gather API Composition & Partial Degradation π
Aggregator queries 4 microservices concurrently in parallel, gracefully degrading on non-critical timeouts before returning a unified composite payload.
01.1. Why API Composition is Mandatory in Microservices
In a traditional monolithic architecture with a unified relational database, rendering a complex user interface (such as an Order Details Page) is straightforward: a single SQL query executes an internal JOIN across orders, users, payments, and shipments tables in < 5ms.
In a microservices architecture, the Database-per-Service pattern strictly prohibits cross-service database queries. The Order Service cannot query the Payment Service's database tables.
If a mobile client were forced to query each service individually, the client would suffer from:
- Chatty Network Overhead: Sending 5 sequential mobile HTTP requests over high-latency cellular 4G/5G connections (
100-300msRTT per hop). - Battery & Data Drain: Multiple TLS handshakes and duplicate header parsing.
- Client Complexity: Complex client-side join logic that breaks whenever backend schemas change.
The API Composition (or Aggregator) pattern solves this by creating a dedicated server-side aggregation layer (often implemented as a Backend for Frontend / BFF) that fetches data from downstream services and joins the results in memory.
02.2. The Scatter-Gather Parallelism Engine
To minimize user-perceived latency, an API Aggregator must never execute downstream RPC calls sequentially. Instead, it must utilize the Scatter-Gather pattern:
Latency Comparison:
- Sequential Execution:
T_{total} = T_{Order} + T_{User} + T_{Payment} + T_{Shipping} = 15ms + 22ms + 38ms + 25ms = 100ms. - Parallel Scatter-Gather:
T_{total} = \max(T_{Order}, T_{User}, T_{Payment}, T_{Shipping}) + T_{merge} = \max(15, 22, 38, 25) + 3ms = 41ms(> 2.4Γfaster!).
TypeScript Implementation Example with Promise.allSettled:
typescriptinterface CompositeOrderResponse { order: OrderDto; customer: UserProfileDto; payment: PaymentReceiptDto; recommendations: RecommendationDto[]; } export async function getCompositeOrderDetails(orderId: string): Promise<CompositeOrderResponse> { // Step 1: Fetch core order to extract customerId and paymentId const order = await orderService.getOrder(orderId); // Step 2: Scatter-Gather concurrent requests for dependent data const [userResult, paymentResult, recsResult] = await Promise.allSettled([ userService.getUserProfile(order.customerId, { timeoutMs: 300 }), paymentService.getPaymentReceipt(order.paymentId, { timeoutMs: 300 }), recommendationService.getRecommendations(order.items, { timeoutMs: 150 }) // fast timeout ]); // Step 3: Handle partial degradation const customer = userResult.status === 'fulfilled' ? userResult.value : { name: 'Customer', email: order.fallbackEmail }; // Fallback const payment = paymentResult.status === 'fulfilled' ? paymentResult.value : { status: 'UNKNOWN', maskedCard: '****' }; const recommendations = recsResult.status === 'fulfilled' ? recsResult.value : []; // Non-critical fallback: empty recommendations array // Step 4: In-Memory Composition return { order, customer, payment, recommendations }; }
03.3. Failure Handling & Partial Degradation Strategies
When an API Aggregator depends on 4β8 microservices, downstream failures are a mathematical certainty at scale. The aggregator must classify dependencies into two categories:
1. Critical Dependencies (Fail-Closed)
- If the Order Service is unreachable, the aggregator cannot fulfill the request and must return an explicit
404 Not Foundor503 Service Unavailableerror.
2. Non-Critical Dependencies (Fail-Open / Graceful Fallback)
- If the Recommendation Service or Loyalty Points Service experiences a timeout or
500error, the aggregator silently catches the exception, logs a warning metric, and populates the response with a static default or empty array ([]). - The end-user successfully views their order and delivery status without noticing that recommendations failed to load.
04.4. API Composition vs CQRS: When Composition Breaks Down
While API Composition is ideal for single-entity composite views (e.g., viewing Order #123), it encounters severe architectural limits when querying large datasets:
- The Pagination & Sorting Nightmare: If you need to query "Show all orders placed in New York with a total
>100$ sorted by customer loyalty score", the aggregator would have to fetch millions of records from the Order, User, and Payment services and perform an in-memory join and sortβcrashing the aggregator with Out-Of-Memory (OOM) errors. - When to Switch to CQRS (Command Query Responsibility Segregation):
- For complex filtering, cross-domain searching, or paginated aggregate queries, do not use API Composition.
- Instead, use CQRS with Change Data Capture (CDC) to asynchronously project denormalized data into a dedicated search database (e.g., Elasticsearch, OpenSearch, or Postgres Read Replica) pre-joined for instantaneous queries.
βοΈArchitectural Trade-offs & Production Realities
Architectural Advantages
- Allows creating rich, tailor-made composite UI payloads without violating Database-per-Service encapsulation
- Minimizes mobile client network roundtrips and cellular battery consumption through single-endpoint aggregation
- Enables graceful partial degradation: non-critical service timeouts do not break core business flows
Trade-offs & Constraints
- In-memory joins consume substantial aggregator CPU and RAM when processing large data volumes
- Overall request latency is bound to the slowest downstream microservice dependency ($\\max(T_1, T_2, ...)$)
- Unsuitable for complex multi-entity sorting, filtering, and cross-domain pagination (requires CQRS instead)
When a user opens the Netflix TV application, the home screen aggregator concurrently fans out requests to the Video Catalog Service, Personalized Artwork Service, Bookmark Service, and Subtitle Service. If the Personalized Artwork service is slow, the aggregator instantly falls back to default global movie posters within a strict 50ms SLA.
π― Staff+ Engineering Takeaways
- API Composition joins data across isolated microservice databases in memory at the gateway or BFF layer.
- Scatter-Gather concurrency reduces total latency to the duration of the single slowest dependency.
- Implement strict timeouts and graceful fallback defaults for all non-critical downstream dependencies.
- Use API Composition for single-entity views; use CQRS / Elasticsearch for cross-service search and pagination.
Topic Knowledge Assessment π§
Step through 3 scenario questions to test your staff-level grasp.
If an API Aggregator needs data from Service A (15ms), Service B (40ms), and Service C (20ms), what is the total latency if calls are executed using parallel Scatter-Gather vs sequential execution?
How clear and staff-actionable was this system breakdown?