Backend for Frontend (BFF) — Deep Dive
Level: Intermediate
Pre-reading: 05 · API & Communication · 05.04 · API Gateway
What is BFF?
Backend for Frontend is a pattern where each client type gets its own dedicated backend API layer. Each BFF is tailored to its client's specific needs.
graph TD
M[Mobile App] --> MBFF[Mobile BFF]
W[Web App] --> WBFF[Web BFF]
P[Partner API] --> PBFF[Partner BFF]
MBFF --> OS[Order Service]
WBFF --> OS
PBFF --> OS
MBFF --> US[User Service]
WBFF --> US
WBFF --> RS[Recommendation Service]
Why BFF?
The Problem with Single API
One generic API serving all clients leads to:
| Problem | Impact |
|---|---|
| Over-fetching | Mobile downloads data it doesn't need |
| Under-fetching | Web makes multiple calls for one view |
| Conflicting requirements | Mobile needs compact, web needs rich |
| Versioning complexity | Breaking change affects all clients |
| Slow iteration | Backend changes need coordination |
BFF Solution
Each client gets an optimized API:
| Client | BFF Optimization |
|---|---|
| Mobile | Compact payloads, minimal calls, offline support |
| Web | Rich data, aggregated views, real-time updates |
| Partner | Stable contract, full documentation, rate limits |
| Smart TV | Paginated lists, image URLs, simple auth |
BFF Responsibilities
| Responsibility | Description |
|---|---|
| Aggregation | Combine data from multiple services |
| Transformation | Shape responses for client needs |
| Protocol translation | REST for mobile, GraphQL for web |
| Client-specific auth | Different auth flows per client |
| Caching | Cache strategies per client pattern |
| Error handling | Client-appropriate error messages |
BFF Architecture
Per-Client BFF
graph TD
subgraph Clients
M[Mobile]
W[Web]
TV[Smart TV]
end
subgraph BFFs
MBFF[Mobile BFF]
WBFF[Web BFF]
TVBFF[TV BFF]
end
subgraph Services
OS[Orders]
US[Users]
PS[Products]
end
M --> MBFF
W --> WBFF
TV --> TVBFF
MBFF --> OS
MBFF --> US
WBFF --> OS
WBFF --> US
WBFF --> PS
TVBFF --> PS
BFF with GraphQL
graph TD
M[Mobile] -->|REST| MBFF[Mobile BFF]
W[Web] -->|GraphQL| GQL[GraphQL BFF]
MBFF --> Services
GQL --> Services
GraphQL can act as a flexible BFF — clients query what they need.
BFF Implementation
Mobile BFF Example
@RestController
@RequestMapping("/api/v1/dashboard")
public class MobileDashboardController {
private final OrderClient orderClient;
private final UserClient userClient;
@GetMapping
public MobileDashboardResponse getDashboard(@RequestHeader("X-User-Id") String userId) {
// Parallel calls
CompletableFuture<List<Order>> ordersFuture =
CompletableFuture.supplyAsync(() -> orderClient.getRecentOrders(userId, 5));
CompletableFuture<User> userFuture =
CompletableFuture.supplyAsync(() -> userClient.getUser(userId));
// Wait and combine
List<Order> orders = ordersFuture.join();
User user = userFuture.join();
// Transform for mobile — compact payload
return MobileDashboardResponse.builder()
.userName(user.getFirstName())
.orderCount(orders.size())
.orders(orders.stream()
.map(o -> MobileOrderSummary.builder()
.id(o.getId())
.status(o.getStatus())
.total(o.getTotal())
.build())
.toList())
.build();
}
}
Web BFF Example
@RestController
@RequestMapping("/api/v1/dashboard")
public class WebDashboardController {
@GetMapping
public WebDashboardResponse getDashboard(@RequestHeader("X-User-Id") String userId) {
// More data for web
CompletableFuture<List<Order>> ordersFuture =
CompletableFuture.supplyAsync(() -> orderClient.getOrders(userId, 20));
CompletableFuture<User> userFuture =
CompletableFuture.supplyAsync(() -> userClient.getUserWithPreferences(userId));
CompletableFuture<List<Product>> recommendationsFuture =
CompletableFuture.supplyAsync(() -> recommendationClient.getForUser(userId));
// Rich response for web
return WebDashboardResponse.builder()
.user(userFuture.join()) // Full user object
.orders(ordersFuture.join().stream()
.map(this::toDetailedOrder) // Includes items, addresses
.toList())
.recommendations(recommendationsFuture.join())
.analytics(getAnalytics(userId))
.build();
}
}
BFF Ownership
| Model | Description | Trade-offs |
|---|---|---|
| Frontend team owns BFF | Mobile team owns Mobile BFF | Best alignment; may lack backend skills |
| Fullstack team | Team owns client + BFF | Fast iteration; team size grows |
| Dedicated BFF team | Central team manages all BFFs | Consistency; becomes bottleneck |
| Platform team + contracts | Platform provides framework; teams extend | Balance of control and flexibility |
Frontend team ownership preferred
The team building the client understands its needs best. They should control the BFF to iterate quickly.
BFF vs API Gateway vs GraphQL
| Aspect | API Gateway | BFF | GraphQL |
|---|---|---|---|
| Purpose | Cross-cutting concerns | Client-specific API | Flexible queries |
| Aggregation | Limited | Yes, per client | Yes, per query |
| Transformation | Basic | Full control | Client specifies |
| Ownership | Platform team | Frontend team | Backend team |
| Complexity | Low | Medium | Medium |
| Use case | Auth, rate limiting | Client optimization | Multiple clients, flexible |
When to Use Each
| Scenario | Solution |
|---|---|
| Centralized auth, rate limiting | API Gateway |
| Multiple clients, different needs | BFF per client |
| Flexible queries, one backend | GraphQL |
| All of the above | Gateway → BFF → Services |
BFF Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
| BFF with business logic | Duplicates service logic | Keep business logic in services |
| BFF calling BFF | Complexity explosion | BFFs call services only |
| Shared BFF for all clients | Back to generic API problem | One BFF per client type |
| BFF as data cache | Stale data, consistency issues | Cache at edge or service level |
| Too many BFFs | Maintenance burden | Consolidate similar clients |
BFF with Service Mesh
In a service mesh, BFFs are just services. They get the same benefits:
graph TD
subgraph Edge
GW[API Gateway]
end
subgraph Mesh
MBFF[Mobile BFF + Envoy]
OS[Order Service + Envoy]
US[User Service + Envoy]
end
GW --> MBFF
MBFF --> OS
MBFF --> US
BFF sidecars handle mTLS, retries, and observability automatically.
Testing BFFs
| Test Type | Focus |
|---|---|
| Unit | Transformation logic |
| Integration | Service client mocking |
| Contract | BFF ↔ Service contracts (Pact) |
| E2E | Full client → BFF → Service flow |
Contract Testing
@Pact(consumer = "mobile-bff", provider = "order-service")
public RequestResponsePact getOrdersPact(PactDslWithProvider builder) {
return builder
.given("orders exist for user 123")
.uponReceiving("a request for recent orders")
.path("/orders")
.query("userId=123&limit=5")
.method("GET")
.willRespondWith()
.status(200)
.body(newJsonArrayMinLike(1, order ->
order.stringType("id")
.stringType("status")))
.toPact();
}
When should you use BFF vs a single API Gateway?
Use BFF when clients have significantly different needs: different data shapes, different aggregation, different auth flows. Use single gateway when clients are similar or the team doesn't want to maintain multiple services. Consider GraphQL as a middle ground — one backend, flexible queries.
Should the frontend team or backend team own the BFF?
Frontend team is generally preferred. They understand client needs best and can iterate quickly. However, they need backend skills (API design, service integration). A compromise: platform team provides a BFF framework; frontend teams extend it.
How do you avoid duplicating logic across multiple BFFs?
(1) Keep business logic in services — BFFs only aggregate and transform. (2) Shared libraries for common transformation logic. (3) GraphQL as a single flexible BFF. (4) Accept some duplication — it's the cost of client optimization.