Skip to content

    System Design

    Backend for Frontend (BFF): One Backend per User Experience

    Learn what a Backend for Frontend owns and must not own, how it handles fan-out and partial failure, who should run it, and when a gateway or GraphQL is enough.

    By Published 12 min read

    Share

    Standard lessonintermediateBackend System Design

    Before this: The API Gateway Pattern (coming soon)

    What you'll learn: By the end of this lesson you will be able to define a Backend for Frontend (BFF), say what belongs in it and what does not, write an aggregator that returns a partial answer when one dependency is slow, explain why a team should own the BFF of its own interface, describe the BFF's role in browser sign-in, and decide between a BFF, a shared gateway and GraphQL for a given system.

    A hotel has one kitchen. Room service, the restaurant and the banquet hall each have their own maître d', who knows what that kind of guest wants, speaks to the kitchen in the kitchen's terms and brings back a tray shaped for the table. The maître d' does not cook, and the banquet maître d' does not need to agree with room service before changing a menu card. If one big counter served all three, every change to it would need three groups of guests to agree.

    A Backend for Frontend (BFF) is that maître d': a server-side component built for one user experience that sits between the client and the services behind it. Sam Newman's 2015 article, the source of the pattern's name, describes the BFF as tightly coupled to a specific user experience and ideally owned by the team that builds that interface. The Azure Architecture Center page credits Newman and describes the same idea.

    The API gateway and service mesh lesson introduces the BFF in a short section, and ShopStream Stage 8 uses an aggregation endpoint. This lesson is the dedicated treatment: what a BFF owns, how it fails, who runs it and when not to build one.

    Verified against Sam Newman's article, the Azure Architecture Center page (dated 2025-03-19) and RFC 10017 (August 2026), on 2026-11-08.

    The core idea

    One user experience gets one backend that speaks its language. A single general-purpose backend serving a desktop site and a phone app collects competing demands. The Azure page says these produce frequent updates, conflicting changes and a development bottleneck. Newman's account of the same problem adds that a separate team often forms to own the shared backend, and the two sides then wait on each other.

    Newman credits Stewart Gleadow's rule of thumb: "one experience, one BFF." If an iOS and an Android app behave alike, one BFF may serve both; if they diverge, give each its own.

    Three examples run through the lesson, each labelled: a minimal example (an aggregator you can run in Node), a realistic example (ShopStream's shopper and store screens, in "In practice") and a failure example (one slow dependency in a fan-out).

    From one shared backend to one BFF per experienceShopper app to Shopper BFF; Store dashboard to Store BFF; Shopper BFF to Catalogue; Shopper BFF to Orders; Store BFF to Orders; Store BFF to InventoryShopper app: phone, small payloadsShopper appphone, smallpayloadsStore dashboard: web, rich tablesStoredashboardweb, rich tablesShopper BFF: owned by app teamShopper BFFowned by app teamStore BFF: owned by dashboard teamStore BFFowned bydashboard teamCatalogueCatalogueOrdersOrdersInventoryInventory

    Figure 1. One BFF per user experience. How to read this: each client talks only to its own BFF. The BFFs share the services behind them but not each other's code or release schedule.

    What belongs in a BFF, and what does not

    The Azure page is direct: the BFF "should only handle client-specific logic related to a specific user experience", and cross-cutting features such as monitoring and authorization should be abstracted away from it.

    Belongs in the BFF Does not belong in it
    Aggregating several service calls into one reply for a screen Business rules such as pricing or stock reservation
    Shaping and trimming payloads for the device Rules shared by every client
    Client-specific paging and caching choices Cross-cutting perimeter work such as rate limiting and gatekeeping (handled separately)
    The browser session and tokens (see below) A copy of another service's data

    Newman is "torn" about perimeter concerns such as authentication and logging: a generic layer can handle them but adds latency, and keeping them in the BFF makes it self-contained. Either way, the line that matters is between client-specific shaping, which is the BFF's job, and domain logic, which stays in services. The gateway lesson's warning applies: a layer full of business rules becomes a bottleneck every team must edit.

    Fan-out and partial failure

    A BFF's characteristic move is the fan-out: one client request becomes several calls. Newman's advice is to run independent calls in parallel and to handle failures gracefully, for example by omitting stock levels when inventory is down. The client then has to render a partial response correctly. That puts a contract in the reply: which parts are present, and which are missing and why.

    Minimal example (runs in Node). A home screen needs three calls with a 100 ms budget each (illustrative). One dependency is slow:

    const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
    
    function withTimeout(promise, ms, label) {
      const timer = new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} timed out`)), ms));
      return Promise.race([promise, timer]);
    }
    
    const services = {
      catalogue: () => wait(20, { featured: 12 }),
      orders: () => wait(30, { active: 1 }),
      loyalty: () => wait(500, { points: 90 }), // slow dependency
    };
    
    async function homeScreen(budgetMs) {
      const names = Object.keys(services);
      const results = await Promise.allSettled(
        names.map((name) => withTimeout(services[name](), budgetMs, name)),
      );
      const body = {};
      const degraded = [];
      results.forEach((result, i) => {
        if (result.status === "fulfilled") body[names[i]] = result.value;
        else degraded.push(names[i]);
      });
      return { ...body, degraded };
    }
    
    console.log(await homeScreen(100));

    Output, from a real run on Node 26.12.1:

    {
      catalogue: { featured: 12 },
      orders: { active: 1 },
      degraded: [ 'loyalty' ]
    }

    The reply arrives after about the slowest permitted call, 100 ms, rather than 500 ms, and the degraded list tells the client what is missing. One limit of the sketch matters: Promise.race stops waiting but does not cancel the slow call, so a real BFF passes a cancellation signal or deadline down to the call (see failure handling end to end).

    sequenceDiagram accTitle: A BFF fan-out with one slow dependency accDescr: The client sends one request to the BFF. The BFF calls catalogue, orders and loyalty in parallel. Catalogue and orders reply quickly. Loyalty does not reply within its timeout. The BFF returns a partial response that names loyalty as degraded. participant C as Client participant B as BFF participant K as Catalogue participant O as Orders participant L as Loyalty C->>B: GET home screen par in parallel, each with a timeout B->>K: featured items B->>O: active orders B->>L: points balance end K-->>B: featured items O-->>B: active orders Note over B,L: loyalty misses its timeout B-->>C: partial response, degraded: loyalty

    Figure 2. Fan-out with a per-call timeout. How to read this: the three calls start together. The BFF does not wait for the slow one past its budget, and the reply says what is missing so the screen can show a placeholder.

    Who owns it, and how many to build

    Ownership is the point. The Azure page says frontend teams that manage their own BFF control language choice, release cadence, workload prioritisation and feature integration without depending on a central backend team. Newman makes the same case: a team that owns both the UI and its BFF can change the API without cross-team coordination, and can even move logic server-side to avoid waiting on app-store review.

    How many. Newman prefers one BFF per client type and accepts sharing one BFF between two apps only when one team owns the apps and the BFF. He notes teams change more often than systems, so separate BFFs make reassigning ownership easier. The Azure page cautions in the other direction: more services mean more operational overhead, an extra network hop and a likely amount of duplicated code, and it advises weighing duplication against a better-tailored experience.

    Duplication. Newman is relaxed about duplication between services because shared libraries are "a prime source of coupling", and suggests extracting shared logic when it appears a third time, preferably into a domain-focused service rather than a shared library.

    The BFF and browser sign-in

    A BFF also changes where tokens live. RFC 10017 (OAuth 2.0 for Browser-Based Applications, a Best Current Practice published August 2026) names the BFF as the first of three architecture patterns and presents them in decreasing order of security. In it the BFF:

    1. acts as a confidential OAuth client towards the authorization server;
    2. manages access and refresh tokens inside a cookie-based session, so no token is exposed to the browser application;
    3. forwards every request to the resource server with the right access token attached.

    The RFC adds that although the BFF runs server-side, it is a component of the frontend application, and to avoid confusion with API gateways and reverse proxies the point to remember is that the BFF becomes the OAuth client for the frontend. Because tokens are only on the server, malicious script in the page has none to steal; it can still send requests to the BFF from the user's browser, and HttpOnly cookies keep it from reading the session itself. The RFC says the BFF pattern is strongly recommended for business applications and that the other two (a token-mediating backend and a browser-based OAuth client) trade security for simplicity. That gives a BFF a security reason to exist even where payload shaping alone would not justify one.

    BFF, gateway or GraphQL

    Shared gateway BFF per experience GraphQL
    Who shapes the reply Gateway team, one shape Each UI team, its own shape The client, per query
    Cross-cutting work Natural home Kept out of it, handled separately Resolvers and gateway
    Cost Single point of contention More services, duplicated code Cost control and caching work (see API styles)
    Fits when Clients want the same thing Experiences differ and teams are separate Many clients, many shapes

    The Azure page names the limits. A BFF may not be suitable when interfaces make the same or similar requests, or when only one interface uses the backend. It also says that where an organisation uses GraphQL with frontend-specific resolvers, BFF services might add little, and that an API gateway with microservices might be enough in some cases where a BFF is usually recommended. Newman notes that a desktop web client on a capable device can often call services directly, and that a web-only UI justifies a BFF mainly when significant server-side aggregation is needed.

    flowchart TD accTitle: Do you need a BFF accDescr: A decision flow. If only one interface uses the backend or all interfaces make the same requests, a gateway is enough. If clients need different shapes of the same data and one team can own the shaping, use GraphQL or a BFF depending on team structure. If the interfaces differ and teams are separate, use a BFF per experience. S["A new client joins an existing backend"] --> A{"Do the interfaces make the same or similar requests?"} A -->|yes| G["Keep one gateway or backend"] A -->|no| B{"Do the UI teams differ from the service teams?"} B -->|no| G2["Aggregation inside the owning service may be enough"] B -->|yes| C{"Is GraphQL with client-specific resolvers already in place?"} C -->|yes| GQ["Use it; a BFF may add little"] C -->|no| BFF["A BFF per experience, owned by the UI team"]

    Figure 3. A first-pass decision. How to read this: it mirrors the "use when" and "might not be suitable when" lists on the Azure page and Newman's team-structure argument. The shape of the teams is part of the input.

    The other side of this topic

    The BFF is the front end's backend. How a client uses one (calling a single tailored endpoint, handling the degraded parts, caching its replies and keeping a session through cookies) is covered by the Frontend System Design track's lesson on consuming a BFF, which is planned and not yet published.

    In practice: ShopStream's shopper and store screens

    This is the realistic example, an educational scenario on the course's running example. Every figure is illustrative.

    This is a realistic scenario, not a case history.

    The situation

    After Stage 8, ShopStream's gateway serves the shopper app and the store dashboard from one aggregation endpoint. The shopper's home screen needs catalogue highlights, active orders and loyalty points, on a phone at the shared browse peak of about 2,300 requests per second. The store dashboard needs stock tables and open orders with many rows. Changes for one keep breaking the other, and two teams queue for one deployable.

    Starting architecture

    ShopStream before the BFF split: one aggregation endpointShopper app to Aggregation endpoint (small payloads); Store dashboard to Aggregation endpoint (large tables); Aggregation endpoint to Backend servicesShopper appShopper appStore dashboardStoredashboardAggregation endpoint: one releaseAggregationendpointone releaseBackend services: four servicesBackendservicesfour servicessmall payloadslarge tables

    Figure 4. The shared aggregation endpoint. How to read this: both clients with different needs depend on one amber component, so one team's release and one slow call affect both.

    Options considered

    Option Gain Cost
    A. Keep one endpoint, add query flags per client No new service Flags multiply, one team gatekeeps changes
    B. A BFF per experience, owned by each UI team Independent releases and tuning Two services to run, some duplicated code
    C. GraphQL for both clients One flexible endpoint Cost control and caching work, new skills

    The decision

    Option B for two experiences, the shopper app and the store dashboard. The courier app stays on direct calls for now because it makes few calls; it gets a BFF when it needs aggregation of its own. Each BFF owns payload shaping, parallel calls with a per-call timeout and a degraded list. Rate limiting and authentication stay at the gateway; pricing and stock rules stay in the services. Duplicated mapping code is accepted until the same logic appears a third time.

    Rollout and verification

    A rollout would route a small share of shopper traffic to the new BFF while the old endpoint keeps serving the rest, and compare replies field by field. Verification: the share of responses with a non-empty degraded list, the extra latency of the added hop against the old endpoint, and payload size per screen. The Azure page asks teams to review service-level objectives when adding a service because of that hop.

    The 2 a.m. failure

    The page reads: "shopper BFF p99 latency high, store BFF normal". The loyalty service is slow and the shopper BFF calls it with no timeout, so threads pile up behind it. The failure stays on the shopper side, which is the reliability benefit the Azure page describes for isolated BFFs. First checks: per-dependency latency from the BFF, thread-pool use, and whether the timeout from the minimal example is configured. Mitigation: set the per-call timeout, return degraded: ["loyalty"] and give loyalty its own pool. The lasting fix is a rule in the BFF template: every downstream call has a timeout, a cancellation path and a defined partial reply.

    Architect's notes

    • Start from teams, not clients. The case for a BFF grows when UI teams are separate from service teams.
    • Make the partial reply part of the contract, with a degraded list the client can act on.
    • Keep domain logic out. If a BFF computes a discount, move it to a service.
    • Put a timeout and a cancellation path on every downstream call in the BFF template.
    • Accept some duplication and extract on the third occurrence.

    Common mistakes

    • One BFF for every client, which rebuilds the shared backend with a new name.
    • Business rules in the BFF.
    • A fan-out with no timeouts, so the slowest dependency sets the latency.
    • A BFF owned by the service team, so the UI team still waits for another team.
    • Treating the BFF as a security perimeter for the whole system; it is part of the frontend, and services still authorise.

    Key takeaways

    • A BFF is a server-side component built for one user experience and owned by the team that builds it.
    • "One experience, one BFF" is a rule of thumb; team structure decides how many.
    • It shapes and aggregates; domain rules and perimeter work stay elsewhere.
    • Fan-out needs parallel calls, per-call timeouts and an explicit partial reply.
    • In browser sign-in, a BFF can be the OAuth client so tokens never reach the browser (RFC 10017).
    • A shared gateway or GraphQL can be enough when clients make similar requests or GraphQL already fits.

    Practice exercises

    Exercise 1 (30 min). Change the Node example so loyalty takes 50 ms. What does the reply look like, and why does degraded become empty?

    Exercise 2 (45 min). Write the reply contract for the shopper home screen: which fields are required, which may be missing, and what the client shows for each missing part.

    Exercise 3 (1 hour). List the logic in your own system's gateway or aggregation layer and mark each item "client-specific shaping" or "domain rule". Where would each go in a BFF design?

    Exercise 4 (2 hours). Decide whether ShopStream's courier app needs a BFF. Write the decision with the three signals you used.

    Official documentation

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments