system-design

Backend for Frontend (BFF)

One API can’t serve a mobile app, a web app, and a smart TV well. A BFF gives each client its own tailored backend, owned by the team that owns the client.

Prerequisites: API Gateway, Service Decomposition Time to read: ~16 minutes


The problem

You have microservices and several clients — a mobile app, a web SPA, a partner API, a TV app. They share a general-purpose API.

Why one API can’t serve all of them well:

A shared API serving all four ends up as a compromise that serves none of them well:

GET /users/42  →  returns 60 fields
   Mobile needs 5 of them, over-fetches the other 55 on a slow network.
   Web needs 20.
   The home screen then makes 6 more calls to assemble one view → 6 round trips on cellular.

🚨 The two failures here are over-fetching (too much data per call) and under-fetching (too many calls per screen), and one shared API forces both on somebody.


The pattern

Give each client type its own backend, shaped exactly for it.

flowchart TB
    M[Mobile app] --> BM[Mobile BFF]
    W[Web app] --> BW[Web BFF]
    T[TV app] --> BT[TV BFF]
    BM --> S1[Users]
    BM --> S2[Orders]
    BM --> S3[Recommendations]
    BW --> S1
    BW --> S2
    BW --> S3
    BT --> S1
    BT --> S2

Each BFF:

🚨 That last point is the whole idea, and it’s what distinguishes a BFF from an API gateway. The mobile team owns the mobile BFF. When they change a screen, they change their BFF — no ticket to a platform team, no coordination, no waiting.

📐 The mobile home screen goes from 6 cellular round trips (300+ ms each) to 1, with the fan-out to 6 services happening in-datacenter (~1 ms each, in parallel). That’s the difference between a sluggish app and a snappy one.


BFF vs API gateway

🚨 The distinction interviewers probe, and getting it right shows you understand both.

  API Gateway BFF
How many One, shared One per client type
Owned by Platform team The client team
Concerns Auth, rate limiting, routing, TLS — generic Aggregation and response shaping — client-specific
Logic Minimal, cross-cutting Client-tailored, may have real logic
Changes Rarely With the client

They compose. A common setup: a shared gateway handles auth, rate limiting, and TLS; behind it, a BFF per client handles aggregation and shaping.

Client → API Gateway (auth, rate limit) → BFF (aggregate, shape) → services

🎙️ “An API gateway centralizes generic concerns — auth, rate limiting — for everyone. A BFF is per-client and owned by the client team, and it does aggregation and shaping. I’d use a gateway for the cross-cutting stuff and a BFF per client for the client-specific stuff.”


Why not put aggregation in the gateway?

You can, and it’s a common temptation. The reason not to is ownership.

🚨 A shared gateway doing per-client aggregation becomes a bottleneck: every client team must file changes against infrastructure owned by a platform team, and that team becomes the constraint on every client team’s velocity. It also becomes a god object — a single component that knows about every client’s every screen.

The BFF pushes that logic to the team that has the context and the incentive to get it right. The mobile team knows what the mobile home screen needs; the platform team doesn’t and shouldn’t have to.

This is Conway’s Law applied: the architecture (one backend per client) mirrors the org (one team per client).


What it costs

⚖️ BFFs are not free, and the main cost is duplication.

1. Code duplication. The mobile BFF and the web BFF both call the users service, both handle auth, both do error handling. Some of that is genuinely different (shaping); some is copy-paste.

Mitigation: a shared library for the common parts (downstream clients, auth helpers, error formatting), keeping only the client-specific shaping in each BFF. Don’t over-share, though — premature abstraction across BFFs recreates the coupling you were escaping.

2. More services to deploy and operate. Three BFFs is three more things with pipelines, monitoring, and on-call.

3. A risk of business logic leaking in. 🚨 A BFF should aggregate and shape, not own business rules. If the mobile BFF and web BFF start computing prices differently, you have a bug factory. Keep domain logic in the services; keep the BFF thin.

4. Another hop. One more network call in the path. Usually negligible in-datacenter.

5. Proliferation. One BFF per client is reasonable. One BFF per screen is not — you’ve re-fragmented into nano-services.


The alternatives

GraphQL is the most important alternative, and often the better one.

Instead of a hand-built BFF per client, expose a GraphQL endpoint and let each client request exactly the fields and relationships it needs in one query.

# Mobile asks for little; web asks for more — same endpoint, no server change
query { user(id: 42) { name, avatar, recentOrders(first: 3) { total } } }

⚖️ GraphQL vs BFF:

GraphQL

🎙️ “If clients need very different shapes and change frequently, GraphQL lets them self-serve without a backend per client. If the shaping involves real orchestration logic — combining several services with fallbacks and business rules — a BFF gives more control. I’d lean GraphQL for data-fetching flexibility, BFF for orchestration-heavy clients.”

Other options: field selection on REST (?fields=name,avatar — poor man’s GraphQL), or the API composition done at a gateway (with the ownership caveat above).


When to use it

Good fits:

Poor fits:

🎙️ “With one web client, I wouldn’t build a BFF — it’s just the backend. The pattern earns its cost when several client types have genuinely divergent needs and separate teams.”


⚖️ Trade-offs

Decision Gain Cost
BFF per client Tailored responses; client-team ownership; fewer round trips Duplication; more services; risk of logic leaking in
One shared API Simple, one thing to run Over-fetching and under-fetching for everyone
Aggregation in the gateway One place Platform team becomes a bottleneck; god object
GraphQL Clients self-serve their shape; no per-client backend N+1, caching, query-cost control, new technology

In the real world


🚨 Interview traps


🎙️ Soundbites


🛠️ Try it

1. Measure the round-trip cost. Build a screen that needs data from three services. Fetch it two ways: the client makes three calls directly, or the client makes one call to a BFF that fans out in parallel. Simulate a 200 ms cellular RTT on the client side. The difference — 600+ ms vs ~250 ms — is the whole argument.

2. Build two BFFs, then feel the duplication. A mobile BFF and a web BFF over the same three services. Notice how much is shared (downstream clients, auth) and how much is genuinely different (the response shape). Extract the shared part into a library and keep only the shaping per BFF.

3. Compare with GraphQL. Expose the same three services behind a GraphQL layer and let a client request its exact shape. Compare the code you had to write, and then deliberately write an N+1-triggering query and watch the resolver problem appear.


Check yourself

1. What's the difference between a BFF and an API gateway? An **API gateway** is a single shared entry point, owned by a platform team, handling generic cross-cutting concerns — authentication, rate limiting, TLS termination, routing — the same way for every client. A **BFF** is one backend *per client type*, owned by the *client* team, handling client-specific aggregation and response shaping. The defining difference is ownership and specificity: the gateway centralizes what's common; the BFF decentralizes what's client-specific to the team with the context to get it right. They compose — a gateway in front for auth and rate limiting, a BFF behind it per client for shaping.
2. Why not just put aggregation logic in the shared API gateway? Because it recreates the bottleneck the pattern is meant to remove. A shared gateway doing per-client aggregation must be changed by a platform team every time any client team changes a screen — so that platform team becomes the constraint on every client team's velocity, and the gateway becomes a god object that knows about every client's every view. The BFF pushes aggregation to the team that owns the client, which has both the context (they know what the screen needs) and the incentive (their own velocity). It's Conway's Law applied deliberately: one backend per client mirrors one team per client.
3. What are the over-fetching and under-fetching problems? **Over-fetching** is receiving more data than the client needs — a `/users/42` endpoint returning 60 fields when the mobile screen uses 5, wasting bandwidth and battery on a cellular connection. **Under-fetching** is a single endpoint not providing enough, forcing the client to make many calls to assemble one screen — the home view making 6 sequential round trips, each 200–300 ms on cellular. A single general-purpose API forces both problems on some clients, because it's a compromise shaped for none of them. A BFF (or GraphQL) fixes both by shaping responses to exactly what each client renders and aggregating the fan-out server-side.
4. When is GraphQL a better choice than a BFF? When clients mainly need *different data shapes* of the same underlying data and change those needs frequently. GraphQL lets each client request exactly the fields and relationships it wants in one query, with no per-client backend to build or maintain and one schema to evolve — clients self-serve their shape. A BFF is better when the per-client work is *orchestration* rather than just shaping — combining several services with fallbacks, conditional logic, and non-trivial aggregation — where you want full server-side control. GraphQL's costs (N+1 resolvers, harder caching, query-cost control, another technology to run) are the price of that flexibility. Large orgs often start with BFFs and evolve toward federated GraphQL as client count grows.
5. What keeps a BFF from turning into a second copy of your business logic? Discipline and a clear rule: the BFF aggregates and shapes; the services own the domain rules. A BFF should call services, combine their responses, and format the result for its client — nothing more. The failure mode is gradual: a small computation added for convenience, then another, until the mobile BFF and web BFF each compute prices or eligibility slightly differently and you have two divergent implementations of the same rule — a bug factory. Prevention: keep all domain logic in the services (so there's one source of truth), review BFF changes for logic creep, and share the truly common infrastructure (downstream clients, auth) via a library while keeping only shaping per BFF.

Further reading