Backend for frontend (BFF)
A small server, usually owned by the frontend team, that gathers data from many backend services and returns exactly what one screen of one kind of client needs.
The problem: one screen, six services
Picture an online shop with a product page. The page shows the product name and photos, the price, whether it is in stock near you, the star rating with three reviews, a row of "you might also like" items, and the number of items in your cart. It looks like one thing to the user.
Behind it, the backend is split into microservices: small services, each owned by one team, each with its own database and its own API. There is a catalog service, a pricing service, an inventory service, a reviews service, a recommendations service, and a cart service. Six teams, six APIs, six release schedules. That split is good for the backend teams, because each one can ship without waiting for the others.
It is hard on the frontend. To draw one page, the browser now has to talk to six services, each designed for its own team's needs, not for this page. And there are other clients too: an iOS app, an Android app, maybe a TV app.
What goes wrong when the browser calls everything
Let the browser call all six services directly and four problems show up, one after the other.
- Too many round trips. Some calls depend on others: you need the product's category before you can ask for recommendations, and its SKU (stock keeping unit, the id of one sellable variant) before you can ask for stock. On a phone with a 150 ms round trip, two rounds of dependent calls cost 300 ms of pure waiting, even if every service answers instantly.
- Too many bytes. Each service returns its full resource. The catalog sends 40 KB of product data (every locale, every attribute, every image size) when the page needs about 3 KB.
- Too much knowledge in the client. The page has to know six base URLs, six error formats, and the order of the calls. That logic is copied into the web, iOS, and Android apps, and the copies drift apart.
- Secrets in the wrong place. Each service wants an access token, so the browser holds tokens where an XSS (cross-site scripting) bug can read them and send them to an attacker.
The fix: one server that speaks for this screen
A backend for frontend, or BFF, is a small server that sits between one kind of client and all those services. The browser makes one call, GET /pages/product/42. The BFF fans out to the six services over the fast data-center network, picks the 3 KB the page needs, shapes it the way the page wants it, and sends it back in one response.
The services stay general and the client stays simple. The screen-specific glue lives in one place, owned by the people who build the screen. That ownership is what makes a BFF different from a plain proxy.
The pattern grew up at SoundCloud in the early 2010s. Their apps were built on one general-purpose public API, and every new mobile feature meant negotiating changes to it with another team. They moved to one small backend per client experience, owned by the client team. Sam Newman wrote the pattern up in 2015 as "Backends For Frontends", still the standard reference.
Check your understanding · Exercise 1
A product page calls six microservices straight from the browser. Which problems does a BFF in front of them directly address? Select all that apply.
The mental model: a waiter for each dining room
Think of a restaurant kitchen with several stations: a grill, a salad bar, a pastry corner. Each is good at one thing. That is your set of microservices. Guests don't visit each station themselves: a waiter takes one order, collects the plates, and brings them out together, arranged the way that table expects.
A BFF is that waiter. The drive-through window gets its own staff, because its customers want paper bags and short waits, not plates. In the same way, each kind of client (web, mobile, TV) gets its own BFF that knows what it needs. The stations don't change when a new kind of guest arrives; only a new waiter is hired.
Three layers, three jobs
Keep this three-layer picture in your head for the rest of the lesson. Almost every question about BFFs is a question about which layer a piece of logic belongs to.
- Clients draw pixels and handle input. They know about screens, not services. They call one host: their own BFF.
- BFFs know about screens and services. They call services, combine and trim the results, translate errors, handle the user's session, and return one view-shaped response. They hold no data of their own beyond caches and sessions.
- Domain services know about the business: what a price is, how stock is counted, who may see a review. They don't know any screen exists, and they serve every BFF the same way.
What "view-shaped" means
A domain service returns a resource-shaped response: a product with all of its fields, the way the catalog team models it. A BFF returns a view-shaped response: the exact data one screen draws, already joined, already formatted, in the order the screen uses it. A good test: could a component render the response with no extra fetching and no business logic? If yes, it is view-shaped.
What a BFF should not do
Most BFFs return JSON, though a Next.js or Remix server that fetches from services and renders HTML plays the same role. Whatever the format, the waiter does not cook. A BFF that calculates prices or decides discounts has taken a domain service's job. The mobile BFF then needs the same logic, and you have two copies of a business rule that will disagree one day.
Think first
The product page should show "Only 3 left" in red when stock is below 5. Where does each part of that belong: the threshold, the count, the text, the colour?
The count of items in stock is a business fact: it comes from the inventory service. Whether "low stock" starts below 5 is also a business rule (marketing may change it, and every client must agree), so the inventory service, or a shared config it owns, should return something like lowStock: true.
The text "Only 3 left" is presentation for one client and can be built in the BFF (it may differ on a watch screen), and the red colour belongs in the client's design system. The BFF passes { stock: 3, lowStock: true, label: "Only 3 left" }; it never decides on its own what counts as low.
How a request flows, step by step
Let's follow one request for the product page through a BFF, with real numbers. The user is on a phone with a 150 ms round trip to the nearest region. The BFF runs in that region, next to the services, and a call between two servers there takes about 2 ms of network time.
- The browser sends
GET /api/pages/product/42to the web BFF, with its session cookie. One request, to one host it already has a connection to. - The BFF reads the cookie, looks up the session, and finds the user's access token (more on that in the auth chapter). The browser never sees the token.
- Round 1. The BFF calls the catalog service for product 42 (30 ms). It needs the result first, because it carries the SKU and the category id.
- Round 2. With those ids, the BFF calls pricing (20 ms), inventory (25 ms), reviews (60 ms), recommendations (80 ms), and cart (15 ms) at the same time. The round takes as long as its slowest call: 80 ms.
- The BFF picks the fields the page draws, formats the price for the user's locale, and builds one JSON object of about 3 KB.
- It sends the response with a short
Cache-Controlheader (orprivate, no-storeif it holds personal data), and the browser renders the page.
What the handler looks like
The core of a BFF route is short. It awaits the calls that others depend on, runs the rest in parallel, and maps the results into the view. Here is the product page route as a Node handler, with the service clients left out.
export async function productPage(id: string, session: Session) {
// Round 1: everything else needs the SKU and category.
const product = await catalog.get(id, { timeoutMs: 50 });
// Round 2: independent calls run at the same time.
const [price, stock, reviews, recs, cart] = await Promise.allSettled([
pricing.get(product.sku, session.token, { timeoutMs: 90 }),
inventory.get(product.sku, session.storeId, { timeoutMs: 90 }),
reviewsApi.top(id, 3, { timeoutMs: 90 }),
recommendations.forCategory(product.categoryId, { timeoutMs: 90 }),
cartApi.count(session.token, { timeoutMs: 90 }),
]);
return {
title: product.name,
image: product.images.medium,
price: valueOr(price, null, (p) => formatMoney(p, session.locale)),
stock: valueOr(stock, null, toStockBadge),
reviews: valueOr(reviews, [], (r) => r.map(toReviewCard)),
recommendations: valueOr(recs, [], (r) => r.slice(0, 8).map(toTile)),
cartCount: valueOr(cart, null, (c) => c.count),
};
}
Two choices matter later. Promise.allSettled, not Promise.all, means one failed service doesn't throw away the other four answers. And each call has its own timeout, so one slow service can't hold the page hostage.
Latency budget math
The total time is the phone's round trip plus the BFF's critical path: the longest chain of calls that depend on each other. Calls in the same round overlap, so only the slowest one in each round counts.
| Step | Browser calls services directly | Browser calls the BFF |
|---|---|---|
| Round 1 (catalog) | 150 ms network + 30 ms = 180 ms | 2 ms network + 30 ms = 32 ms |
| Round 2 (five calls in parallel) | 150 ms network + 80 ms = 230 ms | 2 ms network + 80 ms = 82 ms |
| Phone to BFF and back | none | 150 ms |
| BFF work (parse, map, serialize) | none | about 5 ms |
| Total | about 410 ms | about 270 ms |
The direct column is also optimistic. If the services live on different hosts, the browser needs a DNS lookup, a TCP connection, and a TLS handshake for each new host. Through a BFF, it reuses one connection to one host.
Flip it around to set a latency budget. Say the API response for this page should arrive within 300 ms. The phone's round trip takes 150 ms of that, and we keep 10 ms for the BFF's own work (5 ms typical, plus a margin). That leaves about 140 ms for the BFF's critical path.
With two rounds, give the catalog about 50 ms (it usually takes 32) and round two about 90 ms (its slowest call, recommendations, usually takes 82). Those numbers become the timeouts in the handler above. If a call's usual time sits close to its timeout, the budget is too tight: remove a round or cache that call.
| Within a 300 ms budget | Value |
|---|---|
| Phone round trips, direct against BFF | 2 against 1 |
| Left for the BFF's critical path, after the phone trip and 10 ms for its own work | 140 ms |
| Catalog timeout (it usually takes 32 ms) | about 50 ms |
| Timeout for each later round (its slowest call usually takes 82 ms) | about 90 ms |
Through the BFF the data arrives in 269 ms, inside the budget. Direct calls take 410 ms, over the budget.
Check your understanding · Exercise 2
A BFF calls service A (40 ms). With A's result it calls B (30 ms) and C (90 ms) in parallel. With B's result it calls D (20 ms). Server-to-server network is 2 ms per call and the phone round trip is 120 ms. Which statements are right? Select all that apply.
Shaping the response: aggregate, trim, translate
Calling services in parallel is the easy part. The real value of a BFF is in what it does with the answers. There are four jobs, and each has a common mistake.
Aggregate: join data from several services
The reviews service returns reviews with an authorId. The page wants the author's display name and avatar, which live in the users service. The BFF joins them.
The trap is the N+1 fan-out: one call for the list, then one call per item. Three reviews means four calls; a page of 50 comments means 51.
Fix it by batching: collect the unique ids from the list, then make one call such as GET /users?ids=7,19,23. If the users service has no batch endpoint, ask for one; that is a reasonable request from a BFF team. Libraries like DataLoader do this collecting and batching for you within one request, and also stop the same id being fetched twice.
Trim: send only what the screen draws
The catalog's product resource is 40 KB because it serves many consumers: search indexing, the warehouse app, every locale. The product page needs the name in one language, one image size, the SKU, and a short description. The BFF keeps those and drops the rest.
Trimming also protects you: fields you don't send can't leak. A supplier's cost price or an internal moderation flag is harmless inside the data center and harmful in a browser's network tab. Build the response from an explicit list of fields (an allowlist). Spreading the whole object and deleting a few fields will one day ship a new field nobody reviewed.
Translate: one vocabulary for the client
Each service has its own conventions: one returns prices in cents, another as decimal strings; one uses snake_case, another camelCase; one signals "not found" with 404, another with 200 and { "error": "NOT_FOUND" }. The BFF translates all of that into one vocabulary and one error format for its client.
Be careful with what you format on the server. Turning 1999 cents into "$19.99" is fine if the BFF knows the user's locale and currency. Turning a timestamp into "3 minutes ago" is usually not: the text is wrong a minute later, and it breaks HTTP caching. Send the ISO timestamp and let the client format relative times with Intl.RelativeTimeFormat.
Shield: absorb backend change
When the pricing team renames a field or splits their service in two, only the BFF changes. This matters most for mobile: once version 5.2 of the iOS app is on a million phones, you can't change what it expects. The mobile BFF keeps serving 5.2's shape while newer versions get new fields, often by reading the app version from a header such as X-App-Version.
{
"title": "Trail Runner 3",
"image": "https://img.example.com/p/42/600.webp",
"price": { "amount": 8900, "currency": "EUR", "display": "89,00 €" },
"stock": { "count": 3, "lowStock": true, "label": "Only 3 left" },
"rating": { "average": 4.6, "count": 1284 },
"reviews": [
{ "author": "Mia K.", "avatar": "https://img.example.com/u/7/48.webp",
"stars": 5, "text": "Light and grippy on wet rock.",
"createdAt": "2026-09-30T08:12:00Z" }
],
"recommendations": [{ "id": "51", "title": "Trail Sock", "price": "12,00 €" }],
"cartCount": 2,
"partial": []
}
The partial array lists sections that failed to load, so the client can show a fallback for them. We'll use it in the partial failures chapter.
Check your understanding · Exercise 3
A BFF builds a comments section of 40 comments, each with an authorId. Which are good choices? Select all that apply.
One BFF per client type
The "F" in BFF is singular on purpose. Sam Newman's version of the pattern is one backend per user experience: one for the web app, one for the mobile apps, one for the TV app. Each is built by, and for, the team that owns that client. That is the main difference from a general-purpose API that tries to serve everyone.
Why clients want different things
It is tempting to think web and mobile show "the same data". They rarely do. The same product page differs in almost every way that shapes an API.
- Screen. The desktop web page shows 8 recommendations and 3 reviews; the mobile app shows 4 and 1; a TV shows a large hero image and no reviews.
- Network. A phone on a train needs the smallest payload and the fewest round trips.
- Release cycle. The web app deploys many times a day and every user gets the new version on the next load. A mobile app ships through store review, and old versions stay in use for months, so the mobile BFF must serve several app versions at once.
- Auth. The browser should hold a cookie, not tokens (next chapter). A native app can keep tokens in the operating system's secure storage (Keychain on iOS, Keystore on Android).
- Extra features. Mobile needs push registration and maybe offline sync; web needs server rendering and SEO metadata; TV needs device pairing.
One shared API that serves all of these grows a parameter for every difference (?client=tv&reviews=0), and every client team negotiates changes with the team that owns it: exactly the bottleneck SoundCloud was escaping.
How many BFFs?
Decision How should you split BFFs across your clients?
What we pick: Start with one BFF per client type, owned by that client's team. Let iOS and Android share one while their screens and release cycles stay close. Split by team only when several teams are waiting on each other inside one BFF. A general API is still right for third-party developers, beside the BFFs rather than in place of them.
What about the duplicated code?
Two BFFs will both call the catalog service and both format prices. Some duplication is the price of independence, and it is usually small. When it grows, push shared business logic down into a service, and share plumbing (service clients, auth helpers, logging) as a library.
Check your understanding · Exercise 4
Your web and mobile teams share one BFF. Which signs say it's time to split it? Select all that apply.
Auth at the BFF: keep tokens off the browser
Most modern backends use OAuth 2.0 and OpenID Connect: the user signs in at an authorization server (Auth0, Okta, Entra ID, Keycloak, or your own), and the app receives an access token (a short-lived credential, often valid for 5 to 60 minutes, sent as Authorization: Bearer ...) and a refresh token (a long-lived one used to get new access tokens without asking the user to sign in again).
A single-page app that does this flow by itself keeps both tokens in the browser, where JavaScript on the page can reach them. One XSS bug or compromised third-party script can send the refresh token to an attacker, who can then call your APIs as that user from their own machine for days.
The token handler pattern
A BFF removes tokens from the browser entirely. The BFF becomes the OAuth client: it runs the sign-in flow and keeps the tokens on the server, and the browser gets only an opaque session cookie that JavaScript can't read. Identity vendors such as Curity call this the token handler pattern.
The IETF standard for this is RFC 10017, "OAuth 2.0 for Browser-Based Applications", published in August 2026 as a Best Current Practice after years as a draft. It describes three architectures in decreasing order of security, and the BFF comes first. It strongly recommends the BFF for business applications, sensitive applications, and apps that handle personal data.
- The user clicks "Sign in". The browser navigates (a full page load, not
fetch) to the BFF's/auth/login. - The BFF stores a random
stateand a PKCE code verifier (PKCE, proof key for code exchange, ties the final code to this login attempt) and redirects to the authorization server. - The user signs in there and is redirected back to the BFF's
/auth/callback?code=...&state=.... - The BFF checks
state, then exchanges the code for tokens in a server-to-server call, authenticating with its own client secret. The tokens never pass through the browser. - The BFF stores the tokens in a server-side session (for example in Redis) and sets a cookie holding only the random session id.
- From then on the browser calls
/api/...with the cookie. The BFF looks up the session, refreshes the access token when needed, and attachesAuthorization: Bearer ...to each service call.
HttpOnly cookie that holds a random id.The session cookie, attribute by attribute
HTTP/1.1 302 Found
Location: /
Set-Cookie: __Host-sid=k7Qm2xV9pL0aR4tN8bWc; Path=/; Secure; HttpOnly; SameSite=Strict; Max-Age=28800
HttpOnly:document.cookieand every other script API can't see it, so XSS can't copy it out.Secure: only sent over HTTPS.SameSite=Strict: only sent on requests that start from your own site, never on afetch, formPOST, or link from another site. That blocks most CSRF (cross-site request forgery). RFC 10017 recommendsStrict, which suits a single-page app whose API calls come from its own pages. UseLaxif the server renders personal HTML on the first visit from an outside link: it also sends the cookie on top-levelGETnavigations.__Host-prefix: the browser only accepts the cookie if it isSecure, hasPath=/, and has noDomainattribute, so a sibling subdomain can't overwrite it. RFC 10017 suggests the newer__Host-Http-prefix, which also requiresHttpOnly, but browser support is still partial.Max-Age=28800: the session lasts 8 hours at most. The refresh token on the server may last longer; the BFF decides.
What this fixes, and what it doesn't
Moving to cookies brings back CSRF, where another site makes the user's browser send a request with their cookie attached. SameSite blocks requests from other sites, but every subdomain of example.com counts as the same site, so a hijacked blog.example.com could still forge requests. That is why RFC 10017 says the BFF must have a CSRF defense of its own.
The usual one: reject API requests that lack a custom header such as X-CSRF: 1. A form on another origin can't add a header, and a cross-origin fetch that tries triggers a CORS preflight the BFF refuses. Checking Origin on writes is a cheap extra layer.
Be honest about XSS. An injected script can still call /api/... while the page is open, with the cookie attached. What it can no longer do is steal a token and use it elsewhere after the tab closes. That limits the damage; you still need output encoding and a Content Security Policy.
Two more jobs fit naturally at the BFF. It can swap the user's token for narrower ones per downstream service (OAuth token exchange, RFC 8693). And logout becomes complete: the BFF deletes the session, revokes the refresh token, and clears the cookie.
Check your understanding · Exercise 5
A web app moves from keeping OAuth tokens in localStorage to a BFF with an HttpOnly session cookie. Which statements are true? Select all that apply.
Partial failures and timeouts
A BFF that calls six services will often find one of them slow or down. If any one failure breaks the whole page, you have made the page as fragile as the weakest of six services. The goal is the opposite: the page degrades one section at a time.
Fan-out makes slow calls common
Say each service answers within 100 ms 99% of the time; the other 1% is its slow tail (its p99, the 99th percentile). One call is slow 1 time in 100.
A page that waits for six such calls is slow whenever any of them is: 1 - 0.99^6, about 5.9% of page loads. With 20 calls, it's about 18%. The page's p95 is now set by the services' p99.
So in a fan-out, rare problems become everyday ones. That's why every call needs a timeout, and why the page must have something to show when a call misses it.
Sort every section: required or optional
For each service call, decide in advance what happens if it fails. There are only a few answers.
| Section | If it fails or times out | What the client shows |
|---|---|---|
| Product (catalog) | Required: the page can't exist without it. Return 503 (or 404 if the product is gone). | An error page with a retry button. |
| Price | Required for buying. Return the page, mark price failed. | The page, with "Price unavailable" and the buy button disabled. |
| Stock | Optional. Serve the last cached value if under 5 minutes old, else omit. | No stock badge. |
| Reviews | Optional. Omit. | The rating hidden, a quiet "Reviews couldn't load". |
| Recommendations | Optional. Fall back to a cached "bestsellers" list. | Bestsellers instead of matching items. |
| Cart count | Optional. Omit; the client fetches it later. | The cart icon without a number for a moment. |
Report degraded sections in the response, for example "partial": ["reviews"], and still answer 200, so the client shows a fallback for those sections only. Count every degraded section on the server too, or reviews can be missing for a week before anyone notices.
200 after about 269 ms.
Timeouts, retries, and circuit breakers
- Timeouts from the budget. Give each call a timeout that fits the latency budget, plus an overall deadline. When it passes, cancel the calls still running (with an
AbortSignalin Node). - At most one retry. Retry only idempotent calls (safe to repeat), only on errors that may be temporary, and only if the budget allows. Three retries on every call can send up to four times the load to a struggling service. See the lesson on retries and backoff.
- Circuit breakers. If half the calls to reviews failed in the last 10 seconds, stop calling it for 30 seconds and serve the fallback at once. The page gets faster and the service gets room to recover.
- No single point of failure. Run several BFF instances behind a load balancer and keep them stateless: sessions live in a shared store such as Redis.
Check your understanding · Exercise 6
A BFF fans out to 10 services, each slow 1% of the time, and the reviews service starts timing out on every call. Which are good responses? Select all that apply.
Caching in and around the BFF
A BFF sits at a good spot for caching: it sees every request from its client, and it knows which parts of a response are the same for everyone and which are personal. The skill is to cache the shared parts hard and the personal parts carefully, at the right layer.
Split shared data from personal data
Most of the product page is the same for every user: name, photos, description, rating, top reviews. Only the cart count, and maybe the price (if members get their own), are per user. If one response holds all of it, that response is personal, and no CDN may cache it.
Two common fixes. First, cache inside the BFF per service call: the catalog result for product 42 is cached for 60 seconds and reused across users, while the cart call is never cached. Second, split the screen into a public route (GET /api/pages/product/42, cacheable at the CDN with Cache-Control: public, s-maxage=60, stale-while-revalidate=300) and a small personal one (GET /api/me/product/42, with Cache-Control: private, no-store).
Lifetimes and layers
- Lifetimes per source. The client's data library (TanStack Query, SWR, Apollo) dedupes within a session; a CDN caches public routes; the BFF caches service results with their own lifetimes: catalog 60 s, stock 10 s, recommendations 5 minutes, cart never.
- Whole-view caching for a few seconds helps on hot pages, but only if the key includes every input that changes the view: locale, store, app version.
- Request-scoped dedupe. Within one request, the same service call is made once even if two parts of the view need it. DataLoader-style batching gives you this.
Two traps: the stampede and the leaked key
When a popular cache entry expires, hundreds of concurrent requests may miss at once and all call the service: a cache stampede. Avoid it by letting one request refresh the entry while the others get the stale copy (the same idea as stale-while-revalidate), or by sharing the in-flight promise for that key.
The worse trap is a cache key that misses a personal input. Cache a view that includes a member price by product id alone, and the first member to load the page sets the price everyone sees for 60 seconds. Every entry needs a clear answer to "who is this for?".
Check your understanding · Exercise 7
Which caching choices are safe for a product page BFF whose view includes the user's cart count? Select all that apply.
BFF, API gateway, or GraphQL federation, and who owns it
Three things sit between clients and services in most large systems, and interviewers like to ask how they differ. They solve different problems, are owned by different teams, and often appear together.
- An API gateway is generic edge plumbing: TLS termination, routing
/orders/*to the orders service, checking that a token is valid, rate limiting, request logs. It knows nothing about screens. One gateway serves every client, and a platform team owns it. Examples: AWS API Gateway, Kong, Envoy, Apigee. - A BFF is screen glue for one client: it knows the product page needs six things, calls them, and shapes the result. The client team owns it. It often sits behind the gateway.
- GraphQL federation builds one shared graph from many parts. Each service team publishes a subgraph (its types and fields), and a router (Apollo Router, for example) splits each client query into calls to the right subgraphs and joins the answers. Clients ask for exactly the fields they need.
| Question | API gateway | BFF | GraphQL federation |
|---|---|---|---|
| Knows about screens? | No | Yes, one client's | No; clients describe screens in queries |
| Who writes the aggregation? | Nobody: it routes | The client team, by hand | The router, from the schema |
| Who owns it? | Platform team | Client team | Each service team owns its subgraph; a platform team owns the router |
| Number of them | Usually one | One per client type | One graph for all clients |
| Holds the user's session? | Rarely; it checks tokens | Often (token handler) | Rarely; usually behind a BFF or gateway that does |
| HTTP caching | Passes through | Easy for GET routes | Harder: queries often use POST; needs persisted queries or field-level cache hints |
Which should you use? A gateway alone is fine for a few services and one client. Gateway plus a BFF per client is the default for most teams, and the easiest to explain in an interview. Federation pays off when many clients and teams keep asking for new combinations of the same data and the organization can invest in schema governance; see the GraphQL lesson for that side.
Ownership: the frontend team runs a server
The pattern only works if the BFF is owned by the team that owns the client. If a separate backend team owns the web BFF, every screen change is again a ticket in another team's queue, and you've rebuilt the shared-API bottleneck with an extra hop. So the web team writes it, deploys it, and is on call for it.
That's a real cost. The team now needs server skills: logging, tracing across service calls, capacity planning, security reviews, and on-call. Many teams write the BFF in TypeScript on Node so the same engineers work on both sides and share types. Next.js route handlers and server components, Remix loaders, and SvelteKit server load functions are all a BFF built into the web app's own deploy.
When it turns into a distributed monolith
A distributed monolith is a system split into services that can't change or deploy independently: all the network costs of microservices, none of the freedom. BFFs drift into it in a few recognizable ways.
- Business logic creeps in. Discount rules or stock math live in the BFF, the mobile BFF copies them, and the copies disagree.
- BFFs call each other. The mobile BFF calls the web BFF's
/productroute to reuse its work, so a web change can break mobile. - Lockstep deploys. Every feature needs the BFF and three services released together, in order. The boundaries are wrong.
- Services shaped by one screen. A service grows endpoints like
/getDataForProductPageV2: it now knows about a screen. - One BFF for everyone. A single "BFF" serves web, iOS, Android, and partners: a general-purpose API with a misleading name.
Check your understanding · Exercise 8
Which of these jobs belong in an API gateway rather than a BFF? Select all that apply.
In real products, and in the interview
Where you'll see it
- SoundCloud is where the name started: client teams got their own backends so they could ship without changing a shared public API.
- Netflix made a similar move for its many TV, console, and phone apps: its 2012 API redesign let device teams write their own server-side adapter code, running inside the API service, to shape responses for each device.
- Next.js, Remix, Nuxt, and SvelteKit apps are often BFFs without the name. If you have used a Next.js route handler to hide an API key from the browser, you have built a small BFF.
- Identity products ship the token handler pattern ready-made, such as Duende's BFF library for ASP.NET Core and Curity's token handler.
When to bring it up
Raise a BFF when the problem has at least one of these: a screen that needs data from several services, several client types with different needs, a mobile client on slow networks, or a web app that must handle sign-in securely. In a component question (design an autocomplete, a carousel) it rarely belongs; in an app question (design a shop, a news feed, a video site) it often does.
Equally, say when you would not add one: a single backend that already serves view-shaped endpoints, a small team with one client, or a page that makes one call. Saying "I'd skip a BFF here because..." is as strong a signal as drawing one.
What to draw and say
- Draw the client, one box labelled "Web BFF (owned by web team)", and three or four services behind it. Add a session store if auth is in scope.
- Write the one endpoint the screen calls, such as
GET /api/pages/product/:id, and sketch its response shape: the frontend's API contract. - Say how the BFF fans out (which call is first, which run in parallel) and the rough latency math.
- Say what happens when a service fails: required versus optional sections, timeouts, a
partialmarker. - If auth is in scope, say "tokens stay in the BFF; the browser gets an
HttpOnlySameSitesession cookie", and mention CSRF.
| Level | What a strong answer covers |
|---|---|
| Junior | What a BFF is, why one call beats six from a phone, that it trims the payload. |
| Mid | Parallel fan-out and the critical path, partial failures with allSettled and timeouts, one BFF per client type, a clear view-shaped contract. |
| Senior | Ownership and on-call cost, the token handler pattern and CSRF, caching split between public and private data, tail latency in fan-out, BFF versus gateway versus federation, and the signs of a distributed monolith. |
Think first
An interviewer says: "Doesn't a BFF just add latency? It's an extra hop." How do you answer?
It adds one short hop inside the data center, about 1 to 5 ms plus a few ms of processing. In exchange it removes phone round trips: if the screen needed two or three rounds of dependent calls from the client, that's 150 to 300 ms saved on a typical mobile connection, plus smaller payloads and fewer connection setups.
Then concede the edge case: if a screen makes a single call to a single service, a BFF does add a hop for no latency gain, and I'd only keep it there for other reasons such as holding the session or shielding the client from backend changes.
Summary
- A BFF is a small server per client type, owned by that client's team, that turns many resource-shaped service APIs into one view-shaped response per screen.
- It wins by moving round trips from the phone (about 150 ms each) to the data center (a few ms each). Total time is one client round trip plus the critical path of dependent calls.
- Shape responses with an allowlist of fields, batch per-item lookups to avoid N+1 fan-out, and keep business rules in services: the BFF decides how data looks, never what is true.
- For web apps, the token handler pattern keeps OAuth tokens in the BFF and gives the browser only an
HttpOnly,Secure,SameSitesession cookie. Add CSRF defenses. - Fan-out makes tail latency common. Give every call a timeout from the budget, use
Promise.allSettled, sort sections into required and optional, and report partial results. - Cache service results inside the BFF with per-source lifetimes, and keep anything personal out of shared caches.
- A gateway is generic edge plumbing, a BFF is one client's screen glue, and GraphQL federation is one shared graph. They often stack.
- Watch for the distributed monolith: business logic in the BFF, BFF-to-BFF calls, lockstep deploys, or one BFF for every client.
In the interview
Questions interviewers ask about this. Answer each out loud first, then compare.
Question 1
What is a backend for frontend, and how is it different from an API gateway?
A BFF is a small server dedicated to one kind of client, owned by that client's team. It calls the domain services the client's screens need, runs independent calls in parallel, trims and joins the results, and returns one view-shaped response per screen. For web apps it often also holds the user's session and OAuth tokens.
An API gateway is generic edge plumbing shared by all clients: TLS, routing, token validation, rate limiting. It knows nothing about screens and a platform team owns it. A common setup has both: gateway at the edge, a BFF per client behind it.
Question 2
Why one BFF per client type instead of one BFF for everyone?
Clients differ in screen size, network, release cycle, and auth. A shared BFF grows client flags on every endpoint and makes teams wait on each other, especially because old mobile app versions pin the response shape for months.
With one BFF per experience, each client team ships end to end. The cost is some duplicated glue code; shared business logic moves down into services and shared plumbing into libraries, never into a shared aggregation service.
Question 3
How would you handle authentication for a single-page app with a BFF?
Use the BFF (token handler) pattern that RFC 10017, OAuth 2.0 for Browser-Based Applications, recommends. The BFF is a confidential OAuth client: it runs the authorization code flow with PKCE, exchanges the code for tokens server to server, keeps them in a server-side session, and sets a __Host- cookie that is HttpOnly, Secure, and SameSite=Strict (or Lax if personal pages render on the first visit from an outside link).
API calls carry the cookie; the BFF attaches the access token to service calls and refreshes it as needed. Because cookies are sent automatically, add CSRF defenses: SameSite, a required custom header on API requests, and an Origin check. XSS can still act while the page is open, but it can no longer steal a long-lived token.
Question 4
One of the six services behind your product page BFF is down. What does the user see?
It depends on which one, decided ahead of time. Each section is required or optional. If the catalog is down, there is no page: return 503 and let the client show an error with retry. If reviews or recommendations are down, return 200 with those sections omitted or replaced by a cached fallback, and list them in a partial field so the client shows a small placeholder.
Every call has a timeout from the latency budget, the BFF uses Promise.allSettled so one failure doesn't discard the rest, and a circuit breaker stops calling a service that keeps failing. Degraded sections are logged and counted so the outage is visible.
Question 5
How do you cache in a BFF when the response contains personal data?
Separate the shared parts from the personal parts. Cache service results inside the BFF, each with its own lifetime (catalog 60 s, stock 10 s, cart never), so most inputs come from cache while the response is still built per user. Or split the screen into a public route that a CDN can cache with s-maxage and a small personal route marked private, no-store.
The main risk is a cache key that misses a personal input, which shows one user's data to others. Every cached entry needs a clear answer to who it is for. Also protect hot keys from stampedes by serving stale while one request refreshes.
Question 6
When does a BFF become a problem rather than a solution?
When it turns into a distributed monolith: business rules living in the BFF and copied across BFFs, BFFs calling each other, features that need the BFF and several services deployed in lockstep, services growing screen-specific endpoints, or one "BFF" serving every client.
Also when it adds a hop without removing any: a screen that makes one call to one service gains nothing. And it fails if a separate team owns it, because client teams are back to waiting in another team's queue.
Practice it
These design problems lean on backend for frontend (bff).
References
- Pattern: Backends For Frontends (Sam Newman) The original write-up of the pattern, including one BFF per user experience.
- The Back-end for Front-end Pattern (Phil Calçado) How the pattern came about at SoundCloud.
- OAuth 2.0 for Browser-Based Applications (RFC 10017) The BFF architecture for browser apps, its cookie rules, and the CSRF defenses it still needs.
- Backends for Frontends pattern (Azure Architecture Center) When to use it and when not to.
- Set-Cookie (MDN)
HttpOnly,Secure,SameSite, and cookie prefixes. - OAuth 2.0 Token Exchange (RFC 8693) Swapping a user's token for narrower downstream tokens.
- Apollo Federation introduction How subgraphs and a router compose one graph.
Read next
- REST for frontendsA way to shape an HTTP API around resources and a few standard verbs, so the frontend can fetch, cache, retry, and handle errors in the same predictable way everywhere.
- GraphQLA typed query language for APIs: the client asks one endpoint for exactly the fields a screen needs, across many related objects, in one round trip, and pays for it with harder caching and more work on the server.
- Authentication and token storageHow a web app proves who the user is on every request after login: sessions or tokens, where the proof lives in the browser, how it is refreshed and revoked, and how OAuth with PKCE and OIDC fit in.
- Deduplication and batchingWhen many parts of a page ask for the same data at once, send one request and share the answer; when they ask for many small pieces at once, collect them into one request. Two small ideas that remove most of the wasted traffic in a component-based app.
- CDN cachingKeeping shared copies of your responses on servers near every user, so most requests never travel to your origin: the biggest lever for global speed and for surviving traffic spikes.
- Retries, timeouts, and backoffHow a client survives a flaky network and a struggling server: give every request a deadline, retry only the failures that can heal, space the retries out with exponential backoff and jitter, and stop retrying when the server or the network is clearly down.