⏱ 21 min read
Microservice landscapes rarely fail with a bang. They fail with a shrug.
A product page gets a little slower. A customer dashboard spins for three seconds instead of one. A call center tool shows a partial address, then “refreshes” into correctness. Nobody panics because every service is technically healthy. CPU is fine. Memory is fine. Error rates are acceptable. Yet the user experience feels as if the system is walking through wet cement.
This is the quiet tax of distributed composition.
At first, API composition looks innocent. One service needs a richer view, so it calls two others. Then a frontend gateway wants a single payload, so it orchestrates five more. Then another team copies the pattern because it’s expedient and the architecture decision record says “services own their data.” Before long, the enterprise has created composition cascades: chains and trees of synchronous calls where one request fans out into many, and those many call many more. The call graph starts to resemble ivy on a brick wall. Beautiful from a distance. Destructive up close.
This article is about that problem: API composition cascades in microservices, why they emerge, where they hurt, and what to do instead. We’ll look at them through a domain-driven design lens, because the real mistake is usually not technical but semantic. We’ll explore migration with a progressive strangler approach, because few enterprises get to redesign from a blank page. And we’ll discuss reconciliation, because eventually every distributed system must choose between pretending the world is consistent and building mechanisms to live with inconsistency honestly.
The short version is simple: composition is not evil, but ungoverned composition is. Use it deliberately, close to a real consumer need, and with clear domain boundaries. Do not let it become your hidden integration layer.
Context
Microservices promised autonomy. Teams could move independently, deploy independently, own their data, and evolve at the pace of the business. In the right setting, that promise is real. But autonomy creates an awkward side effect: no single service naturally owns the full story a user wants to see.
A customer support screen doesn’t want “account data” from one bounded context and “billing status” from another and “shipment exceptions” from a third. It wants a customer narrative. It wants one answer. Users think in workflows and outcomes, not service contracts.
That gap between service-local truth and consumer-level truth is where API composition enters.
An API composition layer, sometimes implemented in an API gateway, backend-for-frontend, aggregator service, or query facade, assembles responses from multiple services into a consumer-shaped payload. It is often the first reasonable answer to a very real need. If you’re building a mobile app, for instance, asking the device to call six services over flaky networks is absurd. A composition layer can reduce chatty interactions, centralize orchestration, and shape data for a specific use case.
The trouble begins when composition is no longer a tactical adapter and becomes the default enterprise reflex.
The architecture then starts to drift. Instead of services aligned to domain capabilities, you get service meshes of procedural dependency. Instead of a clean bounded context model, you get semantic leakage everywhere: order status interpreted by shipping, shipping state interpreted by invoicing, invoicing rules embedded in customer profile aggregation. Every team says they own their service, but nobody owns the business meaning of the assembled result.
That’s why this is an architectural article, not just an integration note. API composition cascades are as much about domain semantics as they are about HTTP hops.
Problem
A composition cascade happens when a service or gateway composes data from other services, which themselves compose data from more services, creating a layered dependency chain for a single request.
What began as a simple fan-out becomes a call waterfall mixed with retries, timeouts, partial failures, and duplicated transformations. The consumer sees one endpoint. The platform executes a distributed negotiation.
Here is the shape of the thing.
The client thinks it made one request. In practice, the system may have made ten or fifteen, some serial, some parallel, some cached, some retried. Add authorization checks, tracing, rate limiting, and circuit breaking, and the “simple” query becomes an operational event.
There are several failure signatures that show up repeatedly:
- Latency multiplication: the response time becomes the sum of critical-path calls, not the average of service health.
- Availability erosion: the aggregate endpoint is only as reliable as the weakest link in the request path.
- Semantic coupling: composition code starts embedding business rules that belong in domain services.
- Query gravity: new consumers keep adding fields to the aggregator, pulling more domains into the same request.
- Debugging opacity: teams monitor their service, but nobody sees the user journey end-to-end.
- Change amplification: a minor response contract change in one service breaks multiple aggregate views.
The most dangerous part is that composition cascades usually emerge from good intentions. Teams want to protect consumers from backend complexity. They want to avoid duplication. They want to reuse existing APIs. Those are sensible instincts. But in distributed systems, sensible instincts can produce brittle structures.
If you need one memorable line, it’s this: a cascade is what happens when reuse outruns ownership.
Forces
Architecture is the art of balancing forces, not worshipping patterns. API composition sits right in the middle of several competing pressures.
Consumer simplicity vs producer autonomy
Consumers want a simple, task-oriented API. Producers want to own small, coherent domain models. Those goals are not naturally aligned. Composition often appears as the diplomatic compromise.
Freshness vs resilience
A synchronous composition gives up-to-the-moment data. That sounds attractive. But every live dependency is also a live failure path. Enterprises often overvalue freshness for data that users would accept as slightly stale if the experience were fast and reliable.
Domain purity vs delivery speed
From a domain-driven design perspective, each bounded context should own its language, invariants, and model. Composition layers can violate that by creating “god views” that blend terms from different contexts without explicit mapping. On the other hand, delivery teams need to ship. A composition facade is often faster than building proper read models.
Reuse vs tailored experiences
A shared aggregator looks efficient. Why build separate query endpoints for mobile, web, and partner APIs? Because shared query layers tend to become least-common-denominator monsters. One endpoint serves everybody badly.
Central governance vs team independence
Platform teams often want to standardize aggregation in the gateway. Product teams want domain-specific composition closer to their workflow. Both positions have merit. Both can be abused.
Transactional truth vs query truth
There is a profound semantic issue here. The “truth” for a command is not the same as the “truth” for a query. Order placement may demand strict consistency inside the Ordering context. A customer timeline screen does not. Mixing those concerns is how query paths become transaction-shaped and expensive.
This is where domain-driven design matters. If your aggregate API is crossing bounded contexts, ask a harder question: are you composing data, or are you inventing a new domain concept that deserves its own model?
Sometimes “Customer 360” is not just a screen. It is a genuine supporting domain with its own semantics, freshness policies, and reconciliation logic. Treating it as a dumb aggregator is the architectural sin.
Solution
The solution is not “never compose.” That would be dogma, and dogma is usually just fear wearing a tie.
The solution is to treat composition as a first-class architectural decision with explicit boundaries, depth limits, and semantic ownership.
There are three broad approaches:
- Thin synchronous composition for simple, low-risk query assembly.
- Materialized read models built from events for complex cross-domain views.
- Hybrid composition where a facade combines a stable read model with a few live lookups.
My opinion is blunt: if the consumer view is important, frequently used, or crosses many bounded contexts, build a read model. Don’t make users pay for your service topology on every request.
A practical rule is this:
- Use synchronous composition when there are few hops, clear ownership, low latency budgets, and tolerable partial failure behavior.
- Move to event-driven read models when the view is cross-domain, high-traffic, analytical or summary-heavy, or operationally critical.
- Use Kafka or another event backbone where state changes from source domains can be published as facts and projected into consumer-oriented models.
Here is the healthier target shape.
In this model, the BFF or experience API is no longer discovering truth by traversing the estate in real time. It is reading preassembled projections shaped for the consumer journey. It may still call Identity live for authorization-sensitive data, but the expensive cross-domain assembly has moved off the critical request path.
This is more than a performance optimization. It is a semantic clarification. The read model owns the query language of the experience. Source services still own their transactional models. That separation is healthy.
Architecture
Let’s break the architecture down into layers.
1. Domain services remain authoritative
Each microservice owns transactional behavior and the invariants of its bounded context. Orders are placed in Ordering. Payments are captured in Billing. Shipments are routed in Logistics. This is classic DDD territory: keep aggregate rules where they belong.
What those services should not do is become informal query brokers for every other domain. Once Ordering starts calling Shipping and Billing to answer “order summary” questions, it stops being purely a domain service and starts becoming a composition hub.
That erosion is subtle and common.
2. Experience-facing APIs shape interactions
A backend-for-frontend or experience API can legitimately tailor data for a UI or channel. This is where payload shaping, authorization filtering, pagination style, and channel-specific concerns belong.
But keep this layer honest. Its job is to shape and orchestrate, not to become the enterprise’s accidental domain model.
Good signs:
- It serves one or a few closely related consumer experiences.
- It has explicit latency budgets.
- It tolerates partial results where appropriate.
- It relies mostly on stable read models.
Bad signs:
- Every team depends on it.
- It contains business policy.
- It performs deep call chains.
- Nobody can explain its semantic ownership.
3. Event-driven projections carry cross-domain views
For consumer views that combine many contexts, projections are the workhorse. Source services emit domain events or integration events to Kafka. Downstream consumers build read models optimized for lookup and filtering. event-driven architecture patterns
This gives you three benefits:
- Performance: query cost is paid at write time or asynchronously, not on every read.
- Resilience: the view survives temporary outages of source services.
- Clarity: freshness becomes a contract, not an accident.
Of course, there is no free lunch. Event-driven projections introduce eventual consistency, replay handling, idempotency, schema evolution, and reconciliation. But those are manageable problems. Cascading synchronous APIs often hide equally hard problems until production traffic exposes them.
4. Reconciliation is part of the architecture, not an afterthought
Enterprises love to say “eventually consistent” with the cheerful vagueness of someone promising to tidy the garage one day. Real systems need reconciliation.
If the projection misses an event, receives duplicates, processes out of order, or applies an incompatible schema, what happens? If the read model says a payment is settled but Billing says it was reversed, which view wins and how is it repaired?
A serious composition architecture includes:
- replayable event streams
- idempotent consumers
- versioned event schemas
- dead-letter handling
- backfill jobs
- audit trails
- periodic comparison between source-of-record and projection
Reconciliation is the tax you pay for decoupling. Pay it upfront.
5. Depth limits and budgets
One underrated governance mechanism is architectural budgeting. Set a maximum synchronous composition depth for consumer-facing requests. For example: EA governance checklist
- BFF may call at most three downstream APIs.
- No downstream API called by the BFF may itself call more than one other API synchronously for the same request.
- Cross-domain summary data must come from a read model after a certain fan-out threshold.
This sounds bureaucratic until you’ve spent six months untangling a “simple” dashboard endpoint with 27 backend calls.
Migration Strategy
Nobody wakes up in a greenfield utopia. Most enterprises arrive here with a tangle of service calls, a brittle gateway, and several important consumer journeys already depending on them. So the migration needs to be progressive, not heroic.
The right strategy is a strangler migration applied to query composition.
Start by identifying the worst cascades, not the ugliest code. Follow the user journeys with the highest traffic, worst tail latency, or highest operational pain. Architecture should solve consequential problems first.
A typical migration sequence looks like this:
Step 1: Isolate the consumer contract
Put an experience API or facade in front of the existing composition mess if one does not already exist. This creates a seam. Consumers get a stable contract while you refactor behind it.
Step 2: Observe before changing
Instrument the cascade. You need actual data:
- p50, p95, p99 latency
- downstream fan-out count
- cache hit rates
- timeout distribution
- partial failure frequency
- field-level usage if possible
Many enterprise teams are shocked to discover that 40 percent of aggregated fields are rarely or never used. Dead data causes real pain.
Step 3: Identify semantic clusters
This is where DDD earns its keep. Don’t build read models by copying whatever the current API happens to return. Instead, identify consumer use cases: “customer overview,” “order tracking,” “account eligibility,” “claims summary.” Each should map to a meaningful domain concept, even if it is a supporting or reporting subdomain.
Step 4: Publish events from source domains
Source services emit integration events for state changes relevant to the view. With Kafka, this often means topics like:
order-createdpayment-settledshipment-delayedcustomer-contact-updated
Be strict about event semantics. An event should describe something that happened in the domain, not just a database row update if you can avoid it.
Step 5: Build projections and run in parallel
Create the read model, hydrate it from events, and compare its output to the legacy composition for the same query. This dual-read period is crucial. It reveals semantic mismatches, missing source events, and stale data edge cases before you cut over.
Step 6: Introduce reconciliation
Do not cut over to event-driven views without a repair strategy. At minimum:
- replay support from Kafka
- reconciliation jobs against source systems
- monitoring for projection lag
- operational procedures for reprocessing
Step 7: Strangle gradually
Route a small percentage of traffic or a narrow subset of queries to the new path. Expand as confidence grows. Retire deep synchronous dependencies one branch at a time.
Progressive migration is less glamorous than “replatforming,” but it works in enterprises because it respects reality: budgets, release windows, audit constraints, and the simple fact that the business still needs to run while architecture improves.
Enterprise Example
Consider a global retailer with three major channels: e-commerce, stores, and customer care. Over a decade, it decomposed a monolith into around 80 services. The decomposition was mostly sensible: catalog, pricing, promotions, inventory, orders, payments, shipping, loyalty, identity, customer profile, returns.
Then came the “Customer 360” initiative.
Executives wanted a single screen for call center agents: recent orders, payment issues, delivery exceptions, loyalty status, active coupons, returns in progress, contact preferences, and fraud alerts. The first version was built quickly as an API gateway composition. One request hit customer profile, order history, loyalty, and returns. Order history then called shipping and payments. Loyalty called promotions. Returns called warehouse exceptions. Fraud alerts came from a separate risk service.
At low volume it worked well enough.
At enterprise volume it became notorious.
Agents in the call center would open a customer record and wait five to eight seconds. Sometimes the page partially loaded, then fields disappeared as retries timed out. During peak holiday traffic, the p99 response time crossed 15 seconds. Teams fought over whose service was causing the issue, but the ugly truth was architectural: no single service was failing badly; the cascade itself was the failure mode.
The retailer changed approach.
First, it defined “Customer Care Overview” as a supporting domain, not a random aggregate. That mattered. It allowed the team to define the language of the screen: “recent fulfillments,” “payment concern,” “loyalty standing,” “open service recovery.” Those were not direct copies of source APIs; they were semantically meaningful query concepts.
Second, it moved cross-domain assembly to Kafka-backed projections. Orders, payments, shipping, loyalty, and returns published events. A projection service built a denormalized read model keyed by customer ID, optimized for call center lookups.
Third, it kept a few live lookups where freshness was essential, such as fraud holds and identity entitlements. Everything else came from the projection.
Fourth, it added reconciliation jobs because warehouse exceptions had awkward edge cases and some older services emitted incomplete events.
The results were unsurprising but dramatic:
- median response time dropped below 400 ms
- p99 fell to under 1.5 seconds
- gateway timeout incidents dropped sharply
- call center productivity improved because agents stopped refreshing screens
- source service load became more predictable
The tradeoff was clear too: the view was sometimes seconds behind. For customer care, that was acceptable, and for the few fields that were not, live lookups remained.
That is the enterprise lesson. Most important cross-domain screens are not transactional truth machines. They are operational views. Treat them accordingly.
Operational Considerations
Good architecture diagrams are clean because they omit the misery. Operations is where the misery lives.
Observability
Composition cascades demand end-to-end tracing. Not just logs. Not just service metrics. You need distributed tracing that shows fan-out, retries, timeout paths, and critical-path latency. If you cannot visualize a request from client to every downstream hop, you are managing superstition.
For event-driven projections, monitor:
- consumer lag
- projection staleness by entity
- dead-letter volume
- replay duration
- schema compatibility issues
Caching
Caching can help, but it often becomes an aspirin for a broken leg. Cache static or slow-changing enrichment data. Do not use ad hoc caches to mask uncontrolled call depth. You’ll just add coherence bugs to latency bugs.
Backpressure and rate limits
Aggregators amplify demand. A spike in one client can hammer many downstreams. Budget concurrency carefully. Apply bulkheads. Rate limit expensive aggregate queries. Protect source services from fan-out storms.
Data governance
Once you build read models, especially customer-centric ones, data governance becomes more serious. You may be combining personally identifiable information, payment hints, support notes, and loyalty status. Retention, masking, subject access requests, and regional residency need deliberate handling. ArchiMate for governance
Schema evolution
Kafka helps decouple producers and consumers, but only if event schemas are governed. Version compatibility matters. So does semantic compatibility. A field can remain present and still change meaning. The latter is more dangerous.
Security and authorization
Cross-domain views often blur authorization boundaries. The fact that a call center agent can see shipping status does not mean they should also see payment instrument details. Keep authorization semantics explicit at the consumer-facing API and in read model design.
Tradeoffs
Every architecture pattern is a trade. API composition cascades are no exception.
Benefits of synchronous composition
- simple to start
- fresh data
- no event infrastructure required
- straightforward for narrow, low-fan-out use cases
Costs of synchronous composition
- fragile latency
- reduced aggregate availability
- operational complexity hidden in call chains
- semantic leakage across bounded contexts
- difficult scaling under traffic spikes
Benefits of read models and projections
- fast queries
- resilient consumer experience
- clear consumer-centric API design
- reduced runtime coupling
- easier support for multiple channels
Costs of read models and projections
- eventual consistency
- reconciliation complexity
- duplicate data storage
- event design and schema governance
- more moving parts
The architectural mistake is not choosing one side. It is pretending you can have all the benefits of both without paying the costs of either.
Failure Modes
There are some recurring ways these designs go wrong.
The accidental enterprise service bus
An API gateway starts doing orchestration, transformation, policy, and business logic. Teams keep adding behavior because it is “central.” Soon the gateway becomes the most critical and least understood system in the estate.
Fake domain events
Teams publish thin database-change events with weak semantics. Projections become tightly coupled to internal schemas and break every time a source service refactors persistence.
Read model without repair
The projection works in happy-path demos. Then a topic retention issue, poison message, or deployment bug leaves data stale for thousands of customers. Without replay and reconciliation, the only fix is apology.
Hybrid view with inconsistent promises
Some fields are live, some projected, but the API contract doesn’t say which. Consumers assume all data is equally fresh and make bad decisions.
Shared mega-aggregator
Instead of multiple experience-specific APIs, the enterprise builds one “unified customer API” for web, mobile, support, partner, marketing, and analytics. It becomes political, slow, and impossible to evolve.
Composition inside domain services
A domain service begins calling several other domains to answer commands or queries, effectively becoming a distributed transaction coordinator without admitting it. This is often the road to temporal coupling and fragile business operations.
When Not To Use
There are places where API composition cascades are simply the wrong tool.
- High-frequency, low-latency operational paths such as checkout pricing, fraud decisioning, or authorization-critical flows. Deep composition here is asking for outages.
- Cross-domain analytical or summary views that are better served by projections or dedicated query stores.
- Scenarios needing stable historical snapshots. Live composition gives you “now-ish,” not a reliable point-in-time narrative.
- Domains with weak semantic alignment. If you cannot define a clear business concept for the aggregate view, don’t build one giant endpoint.
- Organizations without event discipline should also be cautious about jumping to projection-based solutions. But that does not justify deep cascades forever. It means you may need to improve platform capabilities first.
On the other hand, a small BFF composing two or three services for a narrow mobile workflow can be perfectly reasonable. Context matters. Always.
Related Patterns
API composition cascades sit near several other patterns, and it’s worth distinguishing them.
Backend for Frontend (BFF)
A BFF tailors APIs to a specific client experience. It is often the right place for light composition. It becomes dangerous when it evolves into a general-purpose cross-enterprise orchestrator.
API Gateway
Good for routing, policy enforcement, security, and coarse aggregation. Bad as a home for deep business semantics.
CQRS
A natural companion to projection-based solutions. Commands remain in transactional bounded contexts; queries use read-optimized models.
Event Sourcing
Not required. Useful in some domains, but many projection architectures work well with ordinary state-based services that publish integration events.
Saga
Relevant for long-running business transactions, not primarily for read composition. Don’t confuse orchestration of commands with assembly of queries.
Strangler Fig Pattern
Essential for migration. Use it to progressively replace deep synchronous composition with experience APIs and read models without breaking consumers.
Summary
API composition is one of those patterns that starts life as a convenience and ends life as a liability if nobody puts guardrails around it.
The core issue is not that microservices need composition. Of course they do. The issue is that many enterprises compose in the wrong place, for the wrong reasons, and without semantic ownership. They let request paths become integration pipelines. They let gateways become hidden domain models. They let users fund the cost of backend sprawl on every click.
A better approach begins with domain-driven design. Respect bounded contexts. Name cross-domain views as real concepts, not just bundles of fields. Use thin synchronous composition where it fits. Move important, high-fan-out views into Kafka-backed projections and read models. Embrace progressive strangler migration rather than fantasy rewrites. And treat reconciliation as part of the design, because eventual consistency without repair is just deferred failure.
If you remember one thing, remember this: the shape of a query is a business decision, not just a networking decision.
Get that right, and your architecture serves the domain.
Get it wrong, and your APIs will cascade like dominos—quietly, expensively, and always at the worst possible moment.
Frequently Asked Questions
What is a service mesh?
A service mesh is an infrastructure layer managing service-to-service communication. It provides mutual TLS, load balancing, circuit breaking, retries, and observability without each service implementing these capabilities. Istio and Linkerd are common implementations.
How do you document microservices architecture for governance?
Use ArchiMate Application Cooperation diagrams for the service landscape, UML Component diagrams for internal structure, UML Sequence diagrams for key flows, and UML Deployment diagrams for Kubernetes topology. All views can coexist in Sparx EA with full traceability.
What is the difference between choreography and orchestration in microservices?
Choreography has services react to events independently — no central coordinator. Orchestration uses a central workflow engine that calls services in sequence. Choreography scales better but is harder to debug; orchestration is easier to reason about but creates a central coupling point.