Backend for Frontend

One Backend per Client Type

A web dashboard, an Android app and a smartwatch need very different data. One general-purpose API forces the mobile app to make many calls and download fields it never shows. A Backend for Frontend (BFF) is a thin service per client type, owned by that client's team, that composes and shapes data exactly for its screens.

React web app          Android / iOS app          Partner API
        |                        |                         |
     web-bff                 mobile-bff               public-api gateway
  (rich pages, admin)    (small payloads, 1 call     (stable, versioned,
                          per screen, image sizes)     rate-limited)
        \                        |                        /
         +---------- product, order, user, payment services -----+
web-bffmobile-bff
Home screen calls3 (page sections load independently)1 (GET /mobile/home)
Product image1200 px400 px, WebP
Fields per productdescription, specs, reviewsname, price, rating, thumbnail
AuthSession cookie (OAuth2 client, token relay)Bearer token from the app

A Mobile BFF Endpoint

@RestController
@RequestMapping("/mobile")
@RequiredArgsConstructor
public class MobileHomeController {
    private final ProductClient products;
    private final OrderClient orders;
    private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

    // One round trip for the whole home screen: important on slow 3G/4G networks
    @GetMapping("/home")
    public MobileHome home(@AuthenticationPrincipal Jwt jwt) {
        var deals = CompletableFuture.supplyAsync(() -> products.deals(10), executor);
        var lastOrder = CompletableFuture.supplyAsync(() -> orders.latestFor(jwt.getSubject()), executor)
                .exceptionally(ex -> null);                                  // optional section
        return new MobileHome(
                deals.join().stream()
                        .map(p -> new MobileProductCard(p.id(), p.name(), p.price(), p.imageUrl() + "?w=400&fm=webp"))
                        .toList(),
                lastOrder.join() == null ? null : new MobileOrderCard(lastOrder.join().id(), lastOrder.join().status()));
    }
}

public record MobileProductCard(Long id, String name, BigDecimal price, String thumbnail) {}
public record MobileOrderCard(Long id, String status) {}
public record MobileHome(List<MobileProductCard> deals, MobileOrderCard lastOrder) {}

Example

curl -s localhost:8090/mobile/home -H "Authorization: Bearer eyJhbGciOi..."

Example output

{"deals":[{"id":1,"name":"boAt Rockerz 450","price":1499.00,"thumbnail":"https://cdn.shopease.in/p/1.jpg?w=400&fm=webp"},
          {"id":4,"name":"Noise ColorFit Pro","price":3999.00,"thumbnail":"https://cdn.shopease.in/p/4.jpg?w=400&fm=webp"}],
 "lastOrder":{"id":1001,"status":"SHIPPED"}}

BFF Rules and Alternatives

  • Keep business rules in the domain services. A BFF only composes, filters and formats.
  • One BFF per experience, not per screen; owned by the frontend team.
  • Avoid copy-paste between BFFs: shared logic belongs in a domain service.
  • GraphQL (Spring for GraphQL with @QueryMapping and @BatchMapping) is an alternative: one flexible endpoint where each client asks for exactly the fields it needs.