“Why do we have three APIs doing similar things?” is a question I’ve been asked often. The answer is usually: because mobile needs lightweight data, the web UI needs detailed data with pre-computed aggregates, and the partner API needs stability and contract rigor. Trying to serve all three from one API produces Frankenstein endpoints with ?include= parameters that nobody understands.
That’s the problem the BFF pattern solves.
What a BFF is
A Backend for Frontend is an API layer dedicated to one client type. Each client has its own BFF:
[Mobile app] → [Mobile BFF] ─┐
├→ [Core microservices]
[Web app] → [Web BFF] ─┤
│
[Partner API] → [Partner BFF]─┘Each BFF aggregates from the core services and shapes responses for its client. Core services stay stable and generic; BFFs adapt to UI needs.
Why not one API for everyone
Single generic API problems:
- Mobile over-fetches. UI needs name + price; API returns 20 fields.
- Web under-fetches. UI composes home feed from 5 calls.
- Partner stability differs. Partners want 6-month notice of breaking changes; app can iterate weekly.
- Auth differs. Session cookies for web, OAuth for partners, mobile tokens.
Trying to satisfy all of them in one surface produces endpoints bloated in different directions, each client only using 30% of what’s exposed.
BFFs let each client’s needs drive its API.
What lives in a BFF
Legitimate BFF responsibilities:
- Aggregation — call multiple services, combine results
- Response shaping — map internal structures to UI shapes
- Caching — client-appropriate cache layers
- Auth translation — translate session/OAuth/JWT to internal tokens
- Client-specific rate limiting
- Per-client error formatting
What does NOT belong:
- Business logic — compute prices, apply promotions, validate inventory. That stays in core services.
- Data persistence — BFFs are stateless. If you need state, it belongs in a core service.
- Core workflows — place order, charge payment, settle trade.
The rule: BFF is glue, not engine.
Example — a home feed BFF endpoint
@GetMapping("/home-feed")
public Mono<HomeFeedResponse> home(@AuthenticationPrincipal User user) {
return Mono.zip(
profileClient.getProfile(user.id()),
ordersClient.recentOrders(user.id(), 5),
recommendationsClient.topPicks(user.id(), 10),
notificationsClient.unread(user.id())
).map(t -> new HomeFeedResponse(
new UserSummary(t.getT1().name(), t.getT1().avatarUrl()),
t.getT2().stream().map(OrderCard::from).toList(),
t.getT3().stream().map(ProductTile::forMobile).toList(),
t.getT4().size()));
}Four parallel service calls, shaped into one response optimized for the mobile home screen. Core services don’t know about home feeds — they just answer their specific queries.
The trap — BFFs turning into monoliths
Every BFF I’ve seen eventually grew features it shouldn’t have. Symptoms:
- Business logic leaking in
- Inter-BFF coupling
- BFFs calling each other
- BFFs writing to databases directly
- BFFs > 100k lines of code
Prevention:
- One BFF per client type, not per feature. Three BFFs (mobile, web, partner), not thirty.
- Review discipline. Every PR that adds persistence or domain logic to a BFF gets challenged.
- Shared libraries, not shared services. Common aggregation helpers as libs, not another service.
- Rotate ownership. Each BFF should have clear team ownership; rotate to prevent attachment.
BFF vs API Gateway
They’re different:
- API Gateway — routes, auth, rate limiting. Application logic = zero.
- BFF — aggregation, response shaping, client-specific logic.
Most systems have both:
Client → API Gateway (cross-cutting) → BFF (client-specific) → Core servicesGateway is generic; BFF is opinionated per client.
Team ownership
Ideal topology: the team that owns the client also owns the BFF.
- Mobile team owns mobile BFF
- Web team owns web BFF
- Partner integrations team owns partner BFF
This aligns incentives. When mobile needs a new field, mobile team changes mobile BFF; no cross-team coordination required. Core service teams don’t need to care about client-specific needs.
When not to have a BFF
- Single client type — one web app, one API, no point splitting
- Small team — BFFs have operational cost; a 5-person team can’t maintain three
- Very thin UI — if the UI uses core APIs nearly as-is, BFF adds friction without benefit
Start without a BFF. Add one when you can point at specific endpoint requests that exist only because “it’s easier for the client”. That’s the signal the BFF is paying for itself.
GraphQL and BFFs
GraphQL often gets positioned as a BFF alternative: “clients ask for exactly what they need”. Real-world experience: GraphQL either becomes the BFF itself, or requires a BFF to route to downstream GraphQL services. Federation helps but isn’t a silver bullet.
Pragmatic take: pick per situation. For a few clients with stable shapes, BFFs are simpler. For many clients with varying needs, GraphQL’s flexibility wins. Most shops I’ve worked at settled on BFFs for public-facing traffic.
Closing note
BFFs solve a real problem — generic APIs serve nobody well — and introduce a real risk — another layer to maintain. The pattern works when it stays thin and client-specific. It fails when it accumulates business logic or persistence. Treat the BFF as glue; the moment it starts feeling like a real service with its own domain, something has drifted and it’s time to pull logic back down to where it belongs.