API Composition Pattern for Microservice Read Models

How I compose a small number of service APIs for booking/dashboard views, including parallel calls, per-dependency timeouts, partial results and the point where a dedicated read model becomes better.

Romharshan Singh
Romharshan SinghSenior Solution Architect • AI & Cloud Mentor
1 September 20262 min read0 viewsUpdated 1 Sept 2026
API Composition Pattern for Microservice Read Models

API Composition Pattern for Microservice Read Models

The use case

A booking summary page may need:

plaintext
Booking
Payment status
Traveller
Hotel status

The data belongs to different services.

For a low/medium-volume interactive view, I can compose APIs rather than create a new database immediately.

BFF/query service

plaintext
Web
 |
Booking Summary BFF
 |-- Booking Service
 |-- Payment Service
 |-- Customer Service
 `-- Hotel Service

NestJS:

typescript
@Get('/booking-summary/:id')
async summary(
  @Param('id') id: string,
) {
  const deadline = Date.now() + 1500;

  const [
    booking,
    payment,
    traveller,
  ] = await Promise.all([
    this.booking.get(id, deadline),
    this.payment.getForBooking(
      id,
      deadline,
    ),
    this.customer.getTraveller(
      id,
      deadline,
    ),
  ]);

  return {
    id,
    status: booking.status,
    paymentStatus: payment.status,
    travellerName:
      traveller.displayName,
  };
}

Parallel calls reduce serial latency.

Per-dependency budget

I do not let every call use the full page SLA.

plaintext
overall page budget 1500 ms

booking 700 ms
payment 700 ms
customer 500 ms

Calls happen in parallel, leaving budget for gateway/rendering.

Partial failure

For a dashboard, payment status might degrade to UNKNOWN.

typescript
const payment =
  await safeCall(
    () =>
      this.payment.getForBooking(id),
    { status: 'UNKNOWN' },
  );

return {
  booking,
  payment,
  degraded:
    payment.status === 'UNKNOWN',
};

For a financial confirmation page, the same fallback may be unacceptable.

Fallback is a business decision.

When composition becomes wrong

If the page calls:

plaintext
20 services

for every request, the probability of at least one dependency being slow becomes high.

At that point I consider:

  • CQRS read projection;
  • cached aggregate view;
  • search index;
  • materialized reporting model.

N+1 calls

List of 100 bookings:

plaintext
1 call get bookings
100 calls get customer
100 calls get payment

This is a classic composition failure.

Batch endpoints or a prepared read model are better.

Production checklist

  • Keep fan-out small.
  • Call independent dependencies in parallel.
  • Define per-call timeouts.
  • Define partial-result semantics.
  • Trace fan-out.
  • Avoid N+1.
  • Cache stable reference data.
  • Move to projection when traffic/fan-out grows.
  • Do not query another service DB directly.

FAQ

API Composition vs CQRS read model?

Composition reads live services at request time. CQRS maintains a prepared read model.

Where should composition live?

Often in a BFF or dedicated query service.

Can an API Gateway compose?

Technically yes, but I avoid putting complex business/read logic in the generic edge gateway.

- [Microservices Design Patterns I Have Used in Real Enterprise Projects](/articles/microservices-design-patterns-real-enterprise-projects) - [CQRS](/articles/cqrs-command-query-responsibility-segregation-real-systems)
  • Backend for Frontend

  • Database per Service

    Architect's final take

    API composition is useful while the fan-out remains understandable. Once the query graph becomes a distributed join engine, I build a read model.

Was this article useful?

Your feedback helps prioritize deeper technical content.

Romharshan Singh
ABOUT THE AUTHOR

Romharshan Singh

Senior Solution Architect and Full Stack Technology Leader with 20+ years of enterprise engineering experience across AI, cloud, distributed systems, Java, Node.js, React, Angular, Kafka and Kubernetes.