The Story
SoundCloud’s mobile team in 2013 spent more time arguing with the backend team than building features. Every screen on the iPhone app required three or four calls to a shared REST API that was designed for the website — fetching a user profile dragged in fifteen fields the phone never displayed, the homepage feed required four sequential round-trips before the first track could play, and authentication used a desktop-shaped session cookie the iOS networking stack handled badly. When the mobile team asked the API team for tailored endpoints, the answer was no — those endpoints would have to ship with every change reviewed by every consumer team, which meant nothing shipped. So the mobile team built their own backend: a thin Node.js service that consumed the existing internal APIs and produced exactly the shape the iPhone app wanted. They called it a Backend-for-Frontend, and within a year every other client team had one of their own. The pattern was not a new architectural insight — it was a team-topology insight: an API designed for two consumers serves neither well, and the right ownership boundary is one backend per client experience.
The original Backend-for-Frontend post (Phil Calçado at SoundCloud, popularized by Sam Newman) is short because the idea is short: stop trying to make one general-purpose API serve every client. Give each client its own backend, owned by the same team that owns the client. What is interesting is what falls out of that decision — the operational consequences are large, and most teams underestimate how much code duplication and schema governance the pattern introduces.
Related Topics: API-Gateway, API-Protocols-Compared, Microservices, API-Design
1. The Fundamental Problem
A single general-purpose API has to serve every client equally. In practice “every client” is a very heterogeneous list:
- A phone has limited radio time, expensive cellular data, a small screen, and an unstable connection. It wants a few large responses, each containing exactly what one screen renders, with aggressive compression.
- A web SPA has cheap bandwidth, a large screen, and a stable connection. It wants many small focused calls so client-side caching is granular, and it tolerates round-trips because they are sub-millisecond on Wi-Fi.
- A smart TV or set-top box has decent bandwidth but weak CPU. It wants pre-rendered, denormalized payloads — shaping data on-device burns frames.
- A partner integration wants a stable, versioned, well-documented contract that does not change without notice, regardless of how the internal services evolve.
- A smartwatch can render maybe three lines of text. It wants one tiny call.
A single API tuned for any of these will be wrong for the others. Tuned for the web, the phone over-fetches and burns battery and data. Tuned for the phone, the SPA can no longer cache granularly. Tuned for partners, internal clients are blocked by external compatibility constraints whenever they want to evolve.
The cost is not just bytes. It is also that every change to the shared API is gated by every consumer team’s review. Cross-team consensus is the actual bottleneck. The pattern’s main payoff is removing that bottleneck, not the bandwidth savings.
2. The Naive Approaches and Why They Fail
Before reaching for BFF, teams typically try three other things. Understanding why each fails clarifies what BFF actually solves.
2.1 One shared REST API
The default approach. One service exposes resource endpoints (/users/{id}, /playlists/{id}); every client composes calls to assemble its screens. Fails because:
- Over-fetching — the response shape is designed for the union of all clients, so each client receives fields it does not use. On mobile this directly costs battery and data.
- Under-fetching — composing a screen requires N round-trips, each adding cellular latency. The home screen becomes “tap → wait → wait → wait → render.”
- Slow evolution — the API team becomes a coordination bottleneck. Schema changes require notice to all consumers; deprecations take years.
2.2 API versioning
When a client needs a different shape, mint a new version (/v2/users/{id}). Eventually the API team supports v1, v2, v3, and v4 simultaneously, with different bug fixes in each, and no team knows which version any given client uses in production. Maintenance cost grows as in versions × consumers. Versioning kicks the schema-governance problem down the road — it does not solve it.
2.3 GraphQL alone
Let each client request exactly the fields it wants via a single graph endpoint. Solves over-fetching cleanly. See GraphQL for the dedicated note; the new problems for the BFF question are:
- Query complexity is pushed to the client. Mobile teams now write and maintain graph queries; mistakes show up as performance regressions in production.
- Caching gets hard. HTTP caching layers cannot cache POST query bodies by URL. Solutions exist (persisted queries, CDN-aware tooling) but add operational complexity.
- Resolver fan-out can make a single GraphQL request produce dozens of internal calls, and per-client rate limiting becomes nonobvious.
- Authorization has to be enforced field-by-field at the resolver layer; getting this right is hard and easy to regress.
GraphQL solves the protocol shape but does not, by itself, solve the team-ownership problem. BFF and GraphQL are commonly combined.
3. The BFF Pattern
3.1 The Core Idea
Build one backend per client experience. Each BFF is a thin server that sits between the client and the underlying microservices, owned by the same team that owns the client. The BFF’s only job is to make the client’s life easy: it aggregates calls, transforms shapes, handles client-specific authentication, and speaks the client’s preferred protocol.
The change is mostly organizational. The mobile team writes mobile code and the mobile BFF, in the same repo, deployed on the same release train. There is no cross-team negotiation when the iOS app needs a new screen — the same team that adds the screen adds the BFF endpoint that serves it. The internal microservices remain general-purpose; they do not change.
3.2 What Sits Behind the BFF
The BFF is the only thing the client talks to. Behind the BFF is the existing microservices fabric — user service, catalog service, recommendations, billing, and so on — exposing internal contracts that are stable and reasonably fine-grained. BFFs translate between the internal contract and the client contract. They do not own business logic; that belongs in the services.
This is the key boundary discipline: if a BFF starts implementing business rules (pricing logic, eligibility checks, fraud rules), the team has built a distributed monolith with the BFF as the new center of gravity. The BFF is a translation layer. Business logic lives in services, where it can be reused across BFFs.
4. Responsibilities of a BFF
A well-scoped BFF owns a small, repeatable set of jobs:
- Aggregation. Combine N internal service calls into one client response. The home screen that took four round-trips becomes one. The BFF can fan out the calls in parallel and assemble the result in the response window.
- Shape transformation. Take rich internal DTOs and produce exactly the fields the client renders. Drop fields the client never uses; flatten nested structures the client cannot navigate efficiently; rename fields to match client conventions if the client team prefers.
- Client-tailored authentication. Different clients have different auth models — short-lived JWTs for mobile, session cookies for web, mTLS or API keys for partners. The BFF terminates the client’s auth model and translates inward to whatever the internal services accept (commonly a service-to-service token).
- Protocol translation. Internal services may speak gRPC for performance, but the mobile BFF exposes JSON over HTTP/2 because that is what iOS networking handles best. The web BFF might expose WebSockets for live updates. Each BFF picks the right edge protocol for its client.
- Client-specific resilience. Circuit breakers, retries, and timeouts are tuned to client tolerance. A mobile client on a flaky network needs aggressive timeouts and a polite degradation strategy (“show last cached content”). A partner integration needs different backoff. Tuning these per-client at the BFF is much easier than at every internal service.
- Edge caching. Per-client cache keys, TTLs, and invalidation rules. The mobile feed might be cached for 60 seconds; the web admin dashboard must be real-time.
What a BFF must not own: business logic, durable state, or any responsibility that requires it to be the source of truth. A rule of thumb: if you removed the BFF and pointed clients directly at the underlying services, the system would be ugly but correct. If removing the BFF would lose data, the BFF has crossed the boundary.
5. BFF vs API Gateway vs GraphQL
These three patterns are confused often enough that it is worth separating them explicitly.
| Pattern | Owns | Lives where | One per | Solves |
|---|---|---|---|---|
| API gateway | Cross-cutting concerns (auth, rate limit, routing, observability) | Edge, in front of everything | Platform | Generic edge concerns; not client-specific shaping |
| BFF | Aggregation, shape, client protocol | Between client and services | Client experience (mobile / web / partner / TV) | Client-specific API shape and team ownership |
| GraphQL gateway | Schema federation, query resolution | Either at edge or behind BFF | Often one per platform; sometimes per BFF | Client-driven shape; pushes query complexity to client |
The patterns layer cleanly. A typical large-scale setup runs an API gateway at the edge (auth, rate limiting, TLS termination), routes per client to a BFF, and the BFF talks to internal services — sometimes via direct REST/gRPC, sometimes via an internal GraphQL gateway that federates across many service schemas. None of these replace the others; they own different concerns.
GraphQL can also be used inside a BFF as an implementation choice — the client speaks REST or gRPC to the BFF, and the BFF uses a federated GraphQL gateway internally to fetch from N services in one shot. This combination preserves the client-side simplicity of REST while exploiting GraphQL’s internal fan-out efficiency.
6. Operational Concerns
The pattern is not free. The honest cost list:
- Code duplication across BFFs. Mobile and web BFFs both need a “load home feed” function, and the two implementations drift. Common defenses: shared client SDKs to the internal services, shared utility libraries, and an explicit principle that duplicated shape code is acceptable but duplicated business logic is not.
- Drift from underlying services. Internal services evolve; if a BFF does not track their changes promptly, it surfaces stale behavior. Treat BFF dependencies on internal services as first-class contracts with consumer-driven contract tests.
- Schema governance. Each BFF’s contract is owned by the client team, but the aggregate contract surface (across all BFFs, all clients) is still a platform concern — partner BFFs in particular need versioning and deprecation discipline. Lightweight central governance prevents each BFF from accidentally exposing PII or undocumented endpoints.
- Deployment coupling with frontend teams. The BFF deploys with the client. This is the intended property — one team, one release train. But it requires the team to operate a backend, with on-call, SLOs, and runbooks. Frontend teams that have never owned a server need help building this muscle.
- More moving parts. N BFFs means N services to deploy, monitor, and scale. The operational footprint grows proportionally to the number of distinct client experiences. Most organizations can sustain this for the four-to-six clients they actually have.
7. When BFF Fits and When It Doesn’t
BFF fits when:
- There are multiple distinct client experiences with materially different needs (mobile + web + TV + partner)
- Cross-team coordination on a shared API has become the bottleneck for product iteration
- The frontend team is mature enough to own a service end-to-end
- The internal microservices are general-purpose and stable enough to be composed by a translation layer
BFF is overkill when:
- There is only one client experience (a single web app) — a normal backend is the BFF, no separation needed
- The client experiences are nearly identical (e.g., responsive web for desktop and mobile) — a shared API still works
- The team is too small to operate multiple backends safely
- The product is in early discovery and the API surface is still volatile — introducing BFFs before the shape stabilizes locks in the wrong boundaries
BFF will fail when:
- Business logic leaks into BFFs and the team loses the discipline of “translation only”
- Cross-BFF duplication is allowed to grow without a shared-library response
- One team owns multiple BFFs and uses them as a backdoor for client-team-specific behaviors that should have been service-level features
8. Technology-Agnostic Comparison
| Dimension | Direct calls (shared REST) | Versioned shared API | BFF | GraphQL gateway (no BFF) | BFF + GraphQL hybrid |
|---|---|---|---|---|---|
| Client over-fetch | High | High | None | None | None |
| Round-trips per screen | Many | Many | One | One | One |
| Schema governance | Centralized, slow | Centralized, slower | Per client team | Centralized graph | Per client team + central graph |
| Cross-team coordination | Required for every change | Required for every version | None (per-client) | Required for graph schema | Reduced (clients own their BFF) |
| Business logic location | Services | Services | Services (must stay there) | Services / resolvers | Services |
| Operational footprint | 1 backend | 1 backend, N versions | 1 BFF per client + services | 1 graph + services | N BFFs + 1 graph + services |
| Caching | HTTP-native, easy | HTTP-native, easy | Per-BFF, flexible | Hard (POST queries) | Per-BFF, flexible |
| Best for | Single-client apps | Slow-moving APIs with few clients | Multi-client products with distinct UX | Internal aggregation across many services | Large products with both needs |
| Worst for | Multi-client products | Anything fast-moving | Single-client apps | Mobile clients writing complex queries | Small teams |
Revision Summary
- A single general-purpose API cannot serve mobile, web, TV, smartwatch, and partner integrations equally — each has different bandwidth, latency, payload, auth, and stability needs. The real bottleneck is cross-team coordination on a shared schema, not just bytes on the wire.
- Naive responses (one shared API, versioning, GraphQL alone) each solve part of the problem and create new ones. Versioning grows . GraphQL solves shape but pushes query complexity to the client and complicates caching and authorization.
- BFF = one backend per client experience, owned by the same team that owns the client. The pattern is primarily a team-topology choice; the operational savings are secondary.
- BFF responsibilities: aggregation (N calls → 1), shape transformation, client-tailored auth, protocol translation, client-specific resilience, edge caching. BFFs must never own business logic or durable state — those belong in services.
- BFF, API gateway, and GraphQL solve different problems and commonly layer together. Gateway at the edge → BFF per client → services (sometimes via an internal GraphQL gateway).
- Costs: code duplication across BFFs, drift from underlying services, schema governance across BFFs, frontend teams having to run backends, N services to operate.
- BFF fits multi-client products with distinct UX needs. It is overkill for single-client apps and dangerous when business logic leaks in.
Deep Understanding Questions
-
Logic leakage. The mobile BFF needs to decide which of two offers to show a user based on tier, time of day, and recent purchase history. The mobile team implements the rule in the BFF “because it’s just a display decision.” Why is this a problem, and what is the right place for that logic? Ans:
-
Cross-BFF duplication. The mobile and web BFFs both implement an “enrich playlist with first-track preview URL” composition. They drift — mobile shows previews from the CDN, web shows them from origin. What are the three options for resolving this, and what are their tradeoffs? Ans:
-
Partner BFF. A partner integration is a kind of client, but partners cannot tolerate breaking changes the way an internal team can. What changes about the BFF’s versioning, deprecation, and schema-governance posture when the client is external? Ans:
-
GraphQL inside the BFF. A mobile BFF talks to twelve internal services to compose the home screen. The team considers replacing twelve gRPC calls with one federated GraphQL query. What does the BFF gain and lose? What does the client see? Ans:
-
Auth translation. The mobile client uses a short-lived JWT; internal services accept an mTLS-authenticated service token. The BFF terminates the JWT and forges the service token. What happens if the BFF is compromised, and what design choices reduce the blast radius? Ans:
-
Caching strategy divergence. The mobile BFF caches the home feed for 60 seconds at the CDN. The web BFF caches it for 5 seconds at the BFF process. A user posts new content and complains that mobile shows stale data for a minute. How do you reason about the right invalidation strategy across BFFs? Ans:
-
Operating cost. A startup has four BFFs and a team of twelve engineers. The on-call rotation is exhausting because every BFF has its own paging surface. What are the structural options for reducing on-call load without collapsing back to a single shared API? Ans:
-
Fan-out failure. The home-screen BFF endpoint fans out to seven internal services in parallel. One service is degraded and times out at 800 ms. The BFF’s outer timeout is 1 second. What does the user see, and what is the right design for partial-degradation responses? Ans:
-
Versioning a BFF. The iOS team wants to ship a major redesign of the home screen that needs a different aggregation. Old iOS clients in the field still need the previous BFF endpoint. How long do you support both, and where does the v1 BFF code live? Ans:
-
BFF vs API gateway boundary. The mobile BFF currently handles rate limiting per user and circuit breaking per downstream service. The platform team is rolling out an API gateway that wants to own both of those. How do you decide what stays at the BFF and what moves to the gateway? Ans:
Discussion
Comments are open. Anonymous is fine — pick any name and post. Comments appear after a quick moderation check.