skip to content

A platform has grown to support three very different client types, a mobile app, a web dashboard, and third-party partner integrations, each needing different composed views over the same dozen backend microservices. What architectural choices exist for where aggregation logic should live, and what are the trade-offs between a per-client Backend-For-Frontend, one shared generic aggregator, and a query-based alternative like GraphQL?

level: principalimportance: nice to knowfreq 35%

answer

  1. BFF equals per-client owned aggregator
  2. shared aggregator risks the god-gateway problem
  3. GraphQL equals query-time aggregation via resolvers
  4. GraphQL's own risk is the N+1 resolver problem
  5. team topology drives the ownership choice

basics

~20 s

You can build one aggregator per client type, each owned by that client's team, one shared aggregator for everyone, or replace hand-written aggregation with a query language like GraphQL where the client asks for exactly the fields it needs. Each trades ownership clarity and flexibility against operational overhead.

solid answer

~40 s

A per-client BFF gives each client team a dedicated aggregation surface it fully owns and can shape or version independently, at the cost of some duplicated composition logic across BFFs. A single shared generic aggregator minimizes duplicate infrastructure but tends to accrete client-specific branches and becomes a cross-team coupling and ownership bottleneck as client needs diverge, the god-gateway problem. A GraphQL-style layer moves the 'which fields, from which services' decision to query time via resolvers, giving clients fine-grained control without a new endpoint per screen, at the cost of adopting a new query engine, resolver-level N+1 call risks, and different caching and rate-limiting characteristics than plain REST aggregation.

go deeper

for a junior

Not generally expected to have an opinion here; recognizing that different clients might need different composed data is enough.

for a middle

Should be aware that BFF is a named pattern where each client type gets its own aggregation layer.

for a senior

Should compare BFF versus shared-aggregator trade-offs concretely, ownership, blast radius, duplication, for a given scenario.

for a principal

Should additionally reason about GraphQL or schema-driven aggregation as a generalization of the same idea, including its own N+1 failure mode, and give a scale-aware recommendation grounded in team topology rather than technology preference alone.

## Three placements for the logic There are three concrete architectural placements for aggregation logic at this scale. 1. **A per-client Backend-For-Frontend** has each client team run its own thin aggregation service tailored to its own client, calling the same shared backend microservices as everyone else but composing responses shaped exactly for that client's screens. 2. **A single shared aggregator** has one team own a handful of generic composed endpoints reused across all client types. 3. **A GraphQL-style gateway** takes a different shape entirely: a schema describes the composable types and fields available, resolvers map each field to whichever backend service supplies it, and the client sends an ad hoc query selecting exactly the fields and nesting it needs, resolved server-side at request time. This last option is worth understanding as a generalized, declarative form of aggregation rather than an unrelated technology; it solves the same 'compose data from many services into one response' problem, just by shifting the shape decision from the gateway's endpoint design to the client's query. ## The choice is organizational, not only technical Which placement is right matters as much organizationally as technically. Aggregation exists to reduce client chattiness, but at this scale the question of where that composition logic lives, and who owns it, becomes just as important as the mechanics of the fan-out itself. Team topology tends to push toward per-client ownership, following the well-known observation that a system's architecture mirrors the communication structure of the organization that builds it, so that a client team can ship changes to its own aggregation surface without waiting on, or risking breaking, every other client. A shared gateway owned by no single client team tends to become a queue of pull requests from every client team instead. ## The trade-off profiles Each option has a distinct trade-off profile. | Placement | What it trades | |---|---| | **Per-client BFFs** | duplicate some composition logic across services in exchange for strong ownership and independent deploy cadence per client team | | **A single shared aggregator** | has lower short-term build cost, one thing to stand up instead of three, but accrues a growing coordination tax and blast radius as client needs diverge: a bug or a slow release train in the shared gateway affects every client at once, and its endpoint code accretes client-specific conditionals over time | | **GraphQL** | removes the need to add a new REST endpoint every time a client needs a new view, since clients simply ask for different fields against the same schema, but it shifts risk into the resolver layer: a naive resolver for a list of items that independently fetches one field per item can silently turn a single client query into hundreds of downstream calls, and the team now has to solve caching, query-depth limiting, and per-field authorization inside the query engine rather than at ordinary endpoint granularity | ## Failure modes 1. **The resolver-level N+1 problem.** A failure mode worth watching for at this level is a team adopting GraphQL purely to 'get away from REST aggregation' without addressing the resolver-level N+1 problem, which reproduces the exact chattiness aggregation was meant to solve, just relocated one layer down and hidden behind a single query that looks efficient from the client's side. The usual fix is batching or dataloader-style resolvers that collect all the per-item field requests for a given query and issue one batched downstream call instead of N separate ones. 2. **The shared aggregator that becomes a god-gateway.** A second, related failure mode is a shared aggregator that started clean and organically becomes the god-gateway anti-pattern as client count grows, at which point the practical fix is usually splitting it into per-client BFFs rather than continuing to layer on conditionals. ## Where it shows up - **Netflix and SoundCloud** are commonly cited as the originators of the BFF pattern, each building separate lightweight aggregation services per device or client team after a single shared API layer became an organizational bottleneck as their client surface diversified. - **GitHub and Shopify** are commonly cited large-scale GraphQL adopters, using a single schema-driven aggregation layer across many client types instead of REST BFFs, accepting resolver complexity in exchange for flexible, client-driven field selection across a very large and varied API surface. For the three-client scenario in this question, a reasonable principal-level recommendation is to start with per-client BFFs, since three clients is a small, manageable number with sharply diverging needs and ownership, and to reach for a schema-driven layer like GraphQL only once the number of distinct client views grows large enough that hand-maintaining per-client REST aggregation endpoints becomes the bigger cost.

  • What is the resolver-level 'N+1 problem' in a GraphQL-style aggregation layer, and how does it echo the original aggregation problem?
    It happens when a query returns a list of N items and a naive resolver fetches an extra field for each item with a separate downstream call, turning one client query into N+1 backend calls instead of one batched call. This recreates exactly the chattiness problem gateway aggregation was invented to solve, just relocated from the client-to-gateway hop down into the gateway-to-backend hop, and is typically fixed with batching or dataloader-style resolvers.
  • Why might a team choose per-client BFFs over one shared aggregator even though it means writing some duplicate composition code?
    Team ownership and independent release velocity are often worth more than avoiding duplication. A dedicated BFF lets one client team change or redeploy its aggregation logic without coordinating with, or risking breaking, every other client's composed endpoints, whereas a shared aggregator turns every client team into a stakeholder on every change.
  • At what point does a single shared generic aggregator typically need to be split apart?
    Usually once its composition logic accumulates enough client-specific conditional branches or optional parameters that a change for one client risks regressing another, or once release cadences across client teams diverge enough that a shared deploy schedule becomes a bottleneck. Both are signs of the god-gateway anti-pattern taking hold and are the usual trigger for splitting into per-client BFFs.

Like choosing between giving each department its own dedicated assistant, a BFF, one overworked shared assistant for the whole company, a shared aggregator, or a self-service request form where anyone can specify exactly which documents they need pulled from the archive, GraphQL -- each has a different mix of dedicated attention versus shared overhead versus request-time flexibility.

saying these in an interview costs you the question

  • treats GraphQL as immune to the N+1 chattiness problem just because it's not REST
  • recommends one shared aggregator for arbitrarily many divergent client types with no mention of ownership cost
  • can't articulate why per-client BFFs trade duplication for independence
  • presents BFF, shared aggregator, and GraphQL as unrelated to the core aggregation problem rather than three placements or mechanisms for the same underlying goal

context