00:00

Backend for Frontend (BFF) Pattern in Microservices

Last updated:

The Backend for Frontend (BFF) pattern gives each type of client its own small backend. The web app talks to a Web BFF, the mobile app talks to a Mobile BFF, and each BFF calls the shared microservices and returns exactly the data its screens need. Instead of one general API that tries to suit every client, each frontend gets an API designed for it.

This tutorial explains the problem the BFF pattern solves, how it works, and how it differs from an API gateway. Three runnable Java examples show response shaping, keeping tokens out of the browser, and supporting old app versions. Interview questions follow at the end.

1. What Is the Backend for Frontend Pattern?

A BFF is a server-side component that belongs to one frontend. It is usually built and owned by the same team that builds that frontend. The pattern was described by Phil Calçado from his work at SoundCloud and made popular by Sam Newman around 2015.

A BFF does three jobs: it combines data from several services, much like the aggregator pattern; it shapes that data for one client's screens; and it handles concerns that are specific to that client, such as browser sessions or mobile app versions.

2. Why One API for All Clients Fails

Different clients need different things from the same data:

  • A mobile app on a slow network wants small responses and as few calls as possible.
  • A web app on a large screen shows more detail, more images and more fields.
  • A smartwatch or partner integration may need only two or three fields.

With one general-purpose API, the mobile app downloads fields it never shows, makes several calls per screen, and joins data on the phone. Every new screen asks the backend team for "just one more field", and changing a response risks breaking another client. The API becomes a compromise that suits nobody, and every frontend team waits on one backend team.

3. How the BFF Pattern Works

Backend for Frontend architectureA web app in the browser and a mobile app on Android and iOS both send requests through an API gateway that handles TLS and rate limiting. The gateway routes web requests to the Web BFF, which returns full detail and manages the browser session, and mobile requests to the Mobile BFF, which returns small payloads. The Web BFF calls the Catalog, Order and Customer services. The Mobile BFF calls only the Catalog and Order services. For the same product, the Web BFF response is 294 characters and the Mobile BFF response is 90 characters. Clients One BFF per frontend Shared services Web app browser Mobile app Android, iOS API Gateway TLS, rate limit Web BFF full detail, session Mobile BFF small payloads Catalog Service Order Service Customer Service response: 294 chars response: 90 chars, same product
Figure 1: each frontend talks to its own BFF, and both BFFs reuse the same services.
  1. The client sends one request to its own BFF, for example GET /mobile/product-page/P-501.
  2. The BFF calls the shared services it needs, in parallel where possible.
  3. It removes unused fields, formats values and combines the results.
  4. It returns one response shaped for that screen.

An API gateway often still sits in front of the BFFs for TLS, rate limiting and routing. The gateway handles concerns shared by everyone; each BFF handles what is specific to its client.

How many BFFs? Create one per user experience, not one per device. If the Android and iOS apps show the same screens, one Mobile BFF serves both. Separate BFFs only pay off when the experiences really differ.

4. The BFF Pattern in Java: Examples

These programs run on Java 21 with no libraries, so you can see the idea without framework code. In a real system each BFF would be a small Spring Boot application calling services over HTTP.

4.1 Shaping One Product for Two Clients

The Catalog Service returns everything it knows. Each BFF keeps only what its screen shows:

ClientSpecificResponses.java
import java.math.BigDecimal;
import java.util.List;

public class ClientSpecificResponses {

    // What the Catalog Service returns: everything it knows about a product
    record Product(String id, String name, BigDecimal price, String description,
                   List<String> imageUrls, double rating, int reviewCount, List<String> specs) {}

    // Each BFF shapes the same product for its own screen
    record MobileCard(String id, String name, BigDecimal price, String thumbnail) {}
    record WebDetail(String id, String name, BigDecimal price, String description,
                     List<String> imageUrls, String ratingText, List<String> specs) {}

    static MobileCard mobileBff(Product p) {
        return new MobileCard(p.id(), p.name(), p.price(), p.imageUrls().getFirst()); // small screen: one image
    }

    static WebDetail webBff(Product p) {
        String rating = p.rating() + " stars from " + p.reviewCount() + " reviews";   // formatted for display
        return new WebDetail(p.id(), p.name(), p.price(), p.description(), p.imageUrls(), rating, p.specs());
    }

    public static void main(String[] args) {
        Product kettle = new Product("P-501", "Steel Kettle 1.7L", new BigDecimal("1499.00"),
                "Fast-boil electric kettle with auto shut-off and a 360-degree base.",
                List.of("/img/p501-front.jpg", "/img/p501-side.jpg", "/img/p501-box.jpg"),
                4.6, 312, List.of("1.7 litre", "2200 W", "Stainless steel"));

        MobileCard card = mobileBff(kettle);
        WebDetail detail = webBff(kettle);

        System.out.println("Mobile BFF -> " + card);
        System.out.println("Web BFF    -> " + detail);
        System.out.println("Mobile response is " + card.toString().length() + " characters, web response is "
                + detail.toString().length());
    }
}
Output
Mobile BFF -> MobileCard[id=P-501, name=Steel Kettle 1.7L, price=1499.00, thumbnail=/img/p501-front.jpg]
Web BFF    -> WebDetail[id=P-501, name=Steel Kettle 1.7L, price=1499.00, description=Fast-boil electric kettle with auto shut-off and a 360-degree base., imageUrls=[/img/p501-front.jpg, /img/p501-side.jpg, /img/p501-box.jpg], ratingText=4.6 stars from 312 reviews, specs=[1.7 litre, 2200 W, Stainless steel]]
Mobile response is 90 characters, web response is 294

The mobile response is less than a third of the size, and the web response arrives with the rating already formatted. Neither client had to change the Catalog Service.

4.2 Keeping Tokens Out of the Browser

This is the BFF's most important security role, and many tutorials miss it. If a single-page app stores access tokens in the browser, any injected script can steal them. With a BFF, the browser keeps only an HTTP-only session cookie, which JavaScript cannot read. The tokens stay on the server:

BffTokenHandler.java
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class BffTokenHandler {

    // Server-side session store inside the BFF: session id -> access token
    static final Map<String, String> sessions = new HashMap<>();

    // After the user logs in, the BFF keeps the token and gives the browser only a random session id
    static String login(String accessToken) {
        // Real BFFs use a random id (UUID.randomUUID or SecureRandom); this one is derived so the output repeats
        String sessionId = UUID.nameUUIDFromBytes(accessToken.getBytes()).toString();
        sessions.put(sessionId, accessToken);
        return "SESSION=" + sessionId + "; HttpOnly; Secure; SameSite=Strict";
    }

    // Every browser request: look up the token and call the downstream service with it
    static String getOrders(String sessionId) {
        String token = sessions.get(sessionId);
        if (token == null) {
            return "401 please log in again";
        }
        return "GET /orders  Authorization: Bearer " + token.substring(0, 8) + "...  -> 200 OK";
    }

    static void logout(String sessionId) {
        sessions.remove(sessionId);   // the token is gone at once; nothing to clean up in the browser
    }

    public static void main(String[] args) {
        String cookie = login("eyJhbGciOiJSUzI1NiJ9.asha.signature");
        String sessionId = cookie.substring("SESSION=".length(), cookie.indexOf(';'));

        System.out.println("Browser stores: Set-Cookie: " + cookie);
        System.out.println("Browser sees a token? " + cookie.contains("eyJ"));
        System.out.println("BFF calls:      " + getOrders(sessionId));

        logout(sessionId);
        System.out.println("After logout:   " + getOrders(sessionId));
    }
}
Output
Browser stores: Set-Cookie: SESSION=2e2c4e24-6c5e-3d9b-a25b-617ae0826256; HttpOnly; Secure; SameSite=Strict
Browser sees a token? false
BFF calls:      GET /orders  Authorization: Bearer eyJhbGci...  -> 200 OK
After logout:   401 please log in again

This is often called the token handler approach. Spring Security supports it with the OAuth2 login client, and Spring Cloud Gateway can forward the stored token with its TokenRelay filter. Because the browser now sends cookies, the BFF must also protect against CSRF, for example with SameSite cookies and CSRF tokens. The session management guide covers sessions in more depth.

4.3 Supporting Old Mobile App Versions

A web app updates as soon as you deploy it, but users keep old mobile apps for months. The Mobile BFF can translate new service responses into the format old versions expect:

AppVersionAdapter.java
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.util.Locale;

public class AppVersionAdapter {

    record Order(String id, BigDecimal total, String status) {}

    // Old app versions in users' phones expect a formatted price string and a different status name
    static String forAppVersion(Order order, int appVersion) {
        if (appVersion < 5) {
            String price = NumberFormat.getCurrencyInstance(Locale.of("en", "IN")).format(order.total());
            String status = order.status().equals("OUT_FOR_DELIVERY") ? "SHIPPED" : order.status(); // v4 does not know the new status
            return "{\"orderId\":\"" + order.id() + "\",\"price\":\"" + price + "\",\"status\":\"" + status + "\"}";
        }
        return "{\"orderId\":\"" + order.id() + "\",\"total\":" + order.total() + ",\"status\":\"" + order.status() + "\"}";
    }

    public static void main(String[] args) {
        Order order = new Order("ORD-1001", new BigDecimal("1499.00"), "OUT_FOR_DELIVERY");

        System.out.println("App 4.2 -> " + forAppVersion(order, 4));
        System.out.println("App 5.0 -> " + forAppVersion(order, 5));
    }
}
Output
App 4.2 -> {"orderId":"ORD-1001","price":"₹1,499.00","status":"SHIPPED"}
App 5.0 -> {"orderId":"ORD-1001","total":1499.00,"status":"OUT_FOR_DELIVERY"}

The Order Service is free to add the new OUT_FOR_DELIVERY status, and version 4 of the app keeps working.

5. BFF vs API Gateway vs General-Purpose API

AspectGeneral-purpose APIAPI gatewayBFF
ServesAll clients the same wayAll clients, one entry pointOne frontend only
Main jobExpose business dataRouting, TLS, rate limits, token checksCombine and shape data for screens
Owned byBackend teamPlatform teamThe frontend's team
Response shapeGenericPassed throughTailored per screen
Client-specific logicGrows into a messShould not contain itIts purpose

They are not competitors. A common setup is a gateway at the edge, a BFF per frontend behind it, and general-purpose services behind the BFFs.

6. Common Mistakes and Pitfalls

  • Business rules in the BFF. Price calculation or stock checks copied into two BFFs will drift apart. Business rules belong in the services; the BFF only shapes data.
  • One BFF per device. Separate Android and iOS BFFs for identical screens double the work for no benefit.
  • Duplicated plumbing. Each BFF writing its own HTTP clients, retries and logging. Share these through a library, not through shared business code.
  • No timeouts or fallbacks. A BFF calls many services, so one slow service can slow every screen. Use the timeout pattern and a circuit breaker.
  • A BFF without an owner. If a separate backend team must change the BFF for every screen, the old bottleneck is back.

7. Best Practices

  • Let the frontend team own its BFF, so they can change screen and API together.
  • Keep the BFF thin: aggregation, shaping and client concerns only.
  • Call services in parallel, with a timeout on every call and fallbacks for optional data.
  • Keep tokens server-side for browser apps, and use secure, HTTP-only, SameSite cookies with CSRF protection.
  • Plan for old mobile versions, and track which versions are still in use before removing support.
  • Trace requests across the BFF with correlation IDs, so you can see which service made a screen slow.

8. Summary

  • The Backend for Frontend pattern gives each type of client its own backend, owned by the frontend team.
  • It solves the one-size-fits-all API problem: over-fetching, too many calls and teams waiting on each other.
  • A BFF combines service data, shapes it for one client and handles client concerns such as sessions and app versions.
  • For browser apps, the BFF keeps tokens on the server and gives the browser only an HTTP-only cookie.
  • Keep business rules in the services, and use the BFF pattern alongside an API gateway, not instead of one.