This website uses cookies

Read our Privacy policy and Terms of use for more information.

Somewhere in your organization there is a Slack thread that goes like this. The mobile team asks for a field on the order endpoint. The web team says adding it will bloat their payload. The backend team says they can ship it in two sprints, and only if both clients agree on the shape. Nobody agrees. Three weeks later the mobile app makes four calls to render one screen, over a cellular connection, because that was the only way to avoid touching the shared endpoint.

That thread is the problem the API Gateway and Backend-for-Frontend patterns exist to solve. They are usually taught as one idea with two names. They are not. A gateway is infrastructure that gives every client a single entry point. A BFF is a piece of product code that belongs to one frontend team. Confusing them is how you end up with a gateway full of business logic that nobody owns, or with six BFFs that all do the same thing.

My argument is that the gateway is a default you adopt almost for free, and the BFF is a decision you should make only when a specific pain shows up. The rest of this post is about how to tell the difference, with code.

The problem both patterns answer

Take a decomposed system: orders, customers, shipments, recommendations, each its own service. A product page needs data from four of them. Without anything in front, the client owns that fan-out.

Chris Richardson's catalog entry frames the question plainly: how do the clients of a microservices application access the individual services? The challenges he lists are the ones you would guess. Clients need data from many services. Different clients need different data shapes. Network performance varies wildly between a phone on 4G and a desktop on fiber. Service locations change. Every one of those pushes work onto the client, and the client is the one place you cannot redeploy on demand. A mobile release goes through app store review. A backend deploy goes through your pipeline in ten minutes.

Microsoft's Gateway Aggregation pattern names the costs of chatty clients directly: more resource use and bandwidth per request, higher latency on cellular and other slow links, and lower reliability because each extra connection is another way for the screen to render half empty. Microservices make this worse, because more services means more calls.

So you put something in front. What you put there is the whole question.

The gateway: one door, thin walls

An API gateway is a single entry point that routes requests to the right service, and in the aggregation case, fans out to several and merges the results. Richardson lists the benefits: clients are insulated from how you partitioned the services, round trips drop (a real win on mobile), clients get simpler, and the gateway can translate between a public protocol and whatever you use internally. The drawbacks are honest too: it is one more thing to develop and operate, and it adds a network hop.

The gateway earns its place with cross-cutting concerns. Authentication, rate limiting, TLS termination, request logging, routing. These are the same for every client and have nothing to do with your product. That is why the gateway is close to free: you are mostly configuring something that exists.

Here is the kind of config where a gateway is at its best. This is Kong's declarative format, but nginx, Envoy, AWS API Gateway, and Azure API Management all express the same idea:

_format_version: "3.0"
services:
  - name: orders
    url: http://orders.internal:8080
    routes:
      - name: orders-route
        paths: ["/v1/orders"]
    plugins:
      - name: rate-limiting
        config: { minute: 600, policy: local }
      - name: jwt
  - name: customers
    url: http://customers.internal:8080
    routes:
      - name: customers-route
        paths: ["/v1/customers"]
    plugins:
      - name: jwt

Every line there is policy, not product. If you can describe what your gateway does as a list of routes and plugins, it is healthy. The moment you find yourself writing a plugin that knows what an "order summary" is, you have started a different project inside your gateway.

Aggregation is where gateways go wrong

The Microsoft pattern page is worth reading for its list of issues, because it reads like a postmortem written in advance. The gateway is a single point of failure, so it must be built for availability. It can become a bottleneck. It must not couple your backend services together. And the one that bites everyone: partial failures. If the order summary needs the order, the shipment, and the customer profile, and the shipment service times out, what does the client get? The pattern page says to use timeouts and think about whether partial data is acceptable, and to apply bulkheads, circuit breakers, and retries so one slow service does not take the whole gateway down.

Here is the shape of that code. This is a Node handler for an order summary, with the failure policy written down instead of left to chance:

async function orderSummary(req, res) {
  const id = req.params.id;
  const timeout = (ms) => AbortSignal.timeout(ms);

  const [order, shipment, customer] = await Promise.allSettled([
    get(`${ORDERS}/orders/${id}`, { signal: timeout(800) }),
    get(`${SHIPMENTS}/by-order/${id}`, { signal: timeout(800) }),
    get(`${CUSTOMERS}/customers/${req.user.id}`, { signal: timeout(500) }),
  ]);

  // The order is the screen. Without it, there is nothing to show.
  if (order.status === "rejected") return res.status(502).end();

  res.json({
    order: order.value,
    // Shipment and customer are decoration. Degrade, do not fail.
    shipment: shipment.status === "fulfilled" ? shipment.value : null,
    customer: customer.status === "fulfilled" ? customer.value : null,
    degraded: [shipment, customer]
      .some((r) => r.status === "rejected"),
  });
}

Notice what this function is. It is not routing, and it is not authentication. It encodes a product decision: the order is mandatory, the shipment and customer are optional, and the client should be told when it got a degraded answer. That is business logic. Put it in the gateway and the platform team now owns a product rule they do not understand and cannot test against the real UI. Microsoft's own guidance leans the same way: consider placing the aggregation in a separate service behind the gateway rather than in the gateway itself.

That separate service is, more or less, a BFF.

The BFF: one backend per experience

The Backend-for-Frontend pattern was described by Sam Newman, who saw it at companies like REA and SoundCloud. The idea is a dedicated backend for each distinct user experience, owned by the team that builds that experience. His rule of thumb is short: one experience, one BFF. If the iOS and Android apps behave differently enough, they get different backends.

Netflix's 2012 API redesign is the clearest early example, even though the term BFF came later. Netflix supported more than 800 device types, each with different memory, markup, screen size, and interaction patterns. A single general-purpose REST API, they wrote, served the provider's convenience rather than the consumer's. Their fix was to separate gathering content from formatting it: each UI team wrote adapter code that ran server-side, called the underlying Java API, and shaped the response for its own device. The post says this cut the number of network requests and improved performance, in some cases by several seconds, though it gives no benchmark table, so I would not quote a percentage.

SoundCloud's story is the cautionary half. They had a monolithic Rails API serving web, Android, iOS, and partners, and the ThoughtWorks write-up of their experience lists the same pain as the Slack thread at the top of this post: frontend teams needed backend approval for every new endpoint. They moved to BFFs so frontend teams could own their API layer, and the backend team provided a lightweight framework handling monitoring, authentication, and rate limiting so that frontend engineers were not rebuilding those from scratch. Then they made the mistake worth learning from. They started with one shared BFF for iOS and Android, and later concluded it should have been two, because the apps were different enough to need different APIs.

A BFF looks boring, which is the point. Here is a mobile one in a few lines:

// mobile-bff: shaped for a phone on a bad connection
app.get("/home", async (req, res) => {
  const [feed, me] = await Promise.all([
    recs.topFor(req.user.id, { limit: 8 }),
    users.profile(req.user.id),
  ]);

  res.json({
    greeting: me.firstName,
    // Phones get small thumbnails and no long descriptions.
    items: feed.map((p) => ({
      id: p.id,
      title: p.title,
      img: p.images.thumb_200,
    })),
  });
});

The desktop BFF for the same screen would return twenty items, full descriptions, and a sidebar. Same downstream services, different shapes, different release schedules, different owners. That is the whole pattern. Microsoft's page puts it the same way: the mobile BFF favors single-page requests, bandwidth efficiency, and caching, while the desktop BFF aggregates several pages' worth of data in one request for a richer experience.

When you do not need a BFF

This is the part the diagrams skip. Microsoft lists the cases where you should not use the pattern: when several interfaces make the same or similar requests, when only one interface uses the backend, and when GraphQL with client-specific resolvers already lets each client ask for exactly what it needs.

That last one deserves attention, because it is the real competitor. A GraphQL layer lets the mobile client request eight items with a thumbnail field and the desktop client request twenty with descriptions, from one schema, with no second service. You pay for it elsewhere: query cost control, caching that is harder than plain HTTP, and a schema team that is now a shared dependency. But if your pain is "different clients need different fields," GraphQL attacks it directly. A BFF is the better answer when the pain is organizational, when the real problem is that one team's roadmap blocks another's.

The costs of the BFF are also on the table in Microsoft's write-up: more services to deploy, maintain, and secure, an extra network hop, and duplication. Newman is blunt about the duplication part. Multiple BFFs will repeat aggregation logic and downstream calls, and the temptation is to merge them into one shared BFF, which brings you back to the shared backend you were escaping. His advice is to wait: extract shared code only after you have written something similar three times, and push aggregation downstream into a real service when creating one is cheap enough. SoundCloud's version is similar: extract shared pieces into libraries when they do not need simultaneous updates, and make a separate service when they do.

The yes, but

The strongest objection to everything above is that the gateway and BFF distinction is academic, because in practice one product does both. AWS API Gateway, Kong, and Apollo Router all let you write transformation logic at the edge, and plenty of teams ship exactly one component that routes, authenticates, and aggregates. Why split it?

Because the failure modes are different. A gateway outage is an outage for every client and every feature. A BFF outage is an outage for one experience. If you collapse them, your blast radius is the union of both, and your ownership is the intersection: nobody owns the product logic and everybody depends on it. I think a single component is fine early, with one client and a few services, as long as you keep the aggregation code in its own module with its own tests. The split becomes worth it when a second client arrives with different needs, or when the platform team and the frontend team start blocking each other. That moment is visible. The Slack thread at the top is the signal.

What to do on Monday

Open your gateway configuration and sort every rule into two piles: policy that applies to every request (auth, limits, routing, TLS) and logic that knows what a product concept is. The first pile stays. The second pile is a BFF you have not named yet, and it belongs in a service owned by the team that ships the screen.

Then ask one question before you add a second BFF: is the pain about response shape, or about who has to approve a change? If it is shape, try GraphQL or a query parameter first. If it is who approves the change, build the BFF, give it to the frontend team, and resist the urge to share code between BFFs until you have written it three times.

Sources