Backend for Frontend (BFF): Tailoring APIs for UI
Mobile apps need less data than Desktop dashboards. Why serve them the same JSON? The Backend for Frontend (BFF) pattern solves this.
Abstract AlgorithmsIntermediate
For developers with some experience. Builds on fundamentals.
Estimated read time: 9 min
AI-assisted content. This post may have been written or enhanced with AI tools. Please verify critical information independently.
TLDR: A "one-size-fits-all" API causes bloated mobile payloads and underpowered desktop dashboards. The Backend for Frontend (BFF) pattern solves this by creating a dedicated API server for each client type β the mobile BFF reshapes data for small screens, the web BFF aggregates for dashboards.
π The One-Size-Fits-All API Problem
You have a mobile app and a web dashboard. Both call the same REST API. The problem:
- Mobile loads a user profile page. The generic
/user/{id}endpoint returns 50 fields. The mobile app uses 8 of them. The remaining 42 fields are wasted bytes over a 4G connection. - Web dashboard needs aggregated data for a rich homepage. Instead of one call, the frontend makes 6 separate API calls (user, orders, recs, notificationsβ¦), assembles the response on the client, and shows a spinner for 3 seconds.
Both problems come from forcing a single API to serve very different clients.
π BFF Basics: One Backend Per Frontend
A generic REST API returns the same response shape to every caller. That works for simple apps but breaks down when clients have radically different needs:
- Mobile apps run on limited bandwidth and battery. They need compact, pre-computed responses.
- Web dashboards need rich, aggregated data combining multiple domain objects.
- Third-party integrations may need specific authentication flows or data formats.
The BFF pattern solves this with a simple rule: each client type gets its own backend.
| Component | Role | Who Owns It |
| Mobile BFF | Serves the mobile app | Mobile team |
| Web BFF | Serves the web dashboard | Web team |
| Partner BFF | Serves external API consumers | Platform team |
| Shared Microservices | Domain business logic | Backend platform team |
The BFF is a translation layer β it speaks the client's language on one side and the microservice language on the other. Business logic stays in the microservices.
βοΈ One API Layer per Client Type
The BFF pattern introduces a thin, client-specific server between each frontend and the shared microservices:
graph TD
Mobile[Mobile App] --> MBFF[Mobile BFF]
Web[Web Dashboard] --> WBFF[Web BFF]
MBFF --> UserSvc[User Service]
MBFF --> CartSvc[Cart Service]
WBFF --> UserSvc
WBFF --> CartSvc
WBFF --> RecSvc[Recommendation Service]
WBFF --> NotifSvc[Notification Service]
Key properties:
- Tight coupling by design: The Mobile BFF is owned by the mobile team. When the app screen changes, the BFF changes with it.
- Data shaping: The BFF projects the microservice response to exactly what the client needs.
- Protocol bridging: Microservices may speak gRPC internally. The BFF exposes REST or GraphQL to the client.
π Request Flow Through a BFF
The request lifecycle from client to microservices flows through the BFF:
flowchart TD
A[Client Request] --> B{Which client?}
B -->|Mobile| C[Mobile BFF]
B -->|Web| D[Web BFF]
C --> E[Auth Token Validation]
D --> E
E --> F[Parallel Upstream Calls]
F --> G[User Service]
F --> H[Cart Service]
F --> I[Rec Service]
G --> J[Response Aggregation]
H --> J
I --> J
J --> K[Field Projection]
K --> L[Client Response]
Each step adds value:
- Auth validation β the BFF verifies the token before hitting any upstream service.
- Parallel upstream calls β fan out to N services simultaneously to minimize latency.
- Response aggregation β merge the N responses into one JSON object.
- Field projection β strip fields the client doesn't use to reduce payload size.
π Mobile BFF vs Web BFF: Request Flow
sequenceDiagram
participant MB as Mobile App
participant MBff as Mobile BFF
participant WB as Web Dashboard
participant WBff as Web BFF
participant US as User Service
participant CS as Cart Service
participant RS as Rec Service
MB->>MBff: GET /mobile/home/{userId}
par parallel fetch
MBff->>US: getUser(userId)
US-->>MBff: firstName only
MBff->>CS: getCart(userId)
CS-->>MBff: itemCount only
end
MBff-->>MB: compact 8-field response
WB->>WBff: GET /web/dashboard/{userId}
par parallel fetch
WBff->>US: getUser(userId)
US-->>WBff: full profile
WBff->>CS: getCart(userId)
CS-->>WBff: full cart detail
WBff->>RS: getTopRecs(userId)
RS-->>WBff: 10 recommendations
end
WBff-->>WB: aggregated dashboard payload
π’ Deep Dive: What Lives Inside a BFF
A BFF is not a full microservice. It is a lightweight orchestration layer. Typical responsibilities:
| Responsibility | Example |
| Request aggregation | Call User + Orders + Recs in parallel, merge response |
| Field projection | Return only the 8 fields the mobile screen uses |
| Format translation | Convert timestamps to "5 min ago" strings for the client |
| Auth token exchange | Trade internal service tokens for client-friendly JWTs |
| Protocol bridging | Expose REST to the client; call gRPC microservices internally |
| Client-specific caching | Cache the mobile home feed; don't cache the admin dashboard |
A BFF should contain no business logic. If you find yourself writing pricing rules or fraud detection inside a BFF, that code belongs in a separate service.
π§ Deep Dive: Parallel Aggregation: The Common BFF Pattern
The most useful thing a BFF does is fan out requests and merge them:
# Mobile BFF: home screen endpoint
async def get_home_screen(user_id: str):
user, cart, recommendations = await asyncio.gather(
user_service.get(user_id),
cart_service.get(user_id),
rec_service.get_top5(user_id),
)
return {
"name": user.first_name,
"cart_count": len(cart.items),
"recs": [r.title for r in recommendations],
}
Three upstream calls run in parallel (~80 ms total instead of sequential ~240 ms). The client gets a single clean response in one round trip.
π§ͺ Practical: Adding a New Mobile Screen
Suppose you add a "notifications center" screen to the mobile app. The workflow with a BFF:
Step 1 β Identify what the screen needs:
- Unread notification count (from Notification Service)
- Top 3 notification previews (from Notification Service)
- User's preferred notification type (from User Service)
Step 2 β Write the BFF endpoint:
# Mobile BFF: notifications screen endpoint
async def get_notifications_screen(user_id: str):
notifs, prefs = await asyncio.gather(
notification_service.get_recent(user_id, limit=3),
user_service.get_notification_prefs(user_id),
)
return {
"unread_count": notifs.total_unread,
"previews": [{"id": n.id, "text": n.text[:50]} for n in notifs.items],
"sound_on": prefs.sound_enabled,
}
Step 3 β The web BFF does NOT need to change. The web dashboard has its own endpoint for notifications with a different shape.
This is the BFF promise: frontend-driven API changes stay contained to that frontend's BFF. No negotiation between mobile and web teams about shared API shape.
π Real-World Application: BFF in Production
- Netflix: Each device type (TV, mobile, browser) has its own backend that shapes API responses to the specific screen and bandwidth constraints.
- SoundCloud: Introduced BFFs to solve exactly the mobile/web mismatch β their blog post coined the pattern in 2015.
- Spotify: Uses BFFs to support desktop, mobile, and embedded devices with different data densities.
βοΈ Trade-offs & Failure Modes: Trade-offs, Failure Modes & Decision Guide
BFF is not always the right answer:
| Situation | Better alternative |
| Single client type (e.g., web only) | One clean REST/GraphQL API |
| Fewer than 3β4 microservices | Direct client calls are simpler |
| Small team that owns both frontend and backend | GraphQL from a single service |
| Clients differ only in auth scopes | API gateway with request transformation |
Code duplication risk: If mobile and web BFFs share 80% of logic, refactor the common parts into shared service clients β not into both BFFs.
π οΈ Spring Boot: Separate BFF Controllers for Web and Mobile Clients
Spring Boot is the standard Java framework for building REST services; its @RestController and reactive WebClient make it straightforward to create separate BFF controller classes β one per client type β that fan out to shared upstream microservice clients and project client-specific response DTOs.
The pattern: a MobileHomeController and a WebDashboardController live in separate Spring Boot modules (or applications), share the same service client beans, but expose different endpoints with different response shapes. Mono.zip fans out upstream calls in parallel β response latency equals the slowest upstream, not their sum.
// βββ Shared upstream client β injected into both BFF controllers βββββββββββ
@Service
public class UserServiceClient {
private final WebClient client;
public UserServiceClient(WebClient.Builder builder) {
this.client = builder.baseUrl("http://user-service").build();
}
public Mono<UserDto> getUser(String userId) {
return client.get().uri("/users/{id}", userId)
.retrieve().bodyToMono(UserDto.class);
}
}
// βββ Mobile BFF β compact payload for small screens ββββββββββββββββββββββ
@RestController
@RequestMapping("/mobile/home")
@RequiredArgsConstructor
public class MobileHomeController {
private final UserServiceClient userClient;
private final CartServiceClient cartClient;
@GetMapping("/{userId}")
public Mono<MobileHomeResponse> getHomeScreen(@PathVariable String userId) {
return Mono.zip(
userClient.getUser(userId),
cartClient.getCart(userId)
).map(t -> new MobileHomeResponse(
t.getT1().getFirstName(), // only first name β not the full profile
t.getT2().getItemCount() // only cart count β not the full cart detail
));
// Parallel fan-out: ~80 ms total instead of sequential ~160 ms
}
}
// βββ Web BFF β rich aggregated payload for the dashboard βββββββββββββββββ
@RestController
@RequestMapping("/web/dashboard")
@RequiredArgsConstructor
public class WebDashboardController {
private final UserServiceClient userClient;
private final CartServiceClient cartClient;
private final RecommendationServiceClient recClient;
private final NotificationServiceClient notifClient;
@GetMapping("/{userId}")
public Mono<WebDashboardResponse> getDashboard(@PathVariable String userId) {
return Mono.zip(
userClient.getUser(userId),
cartClient.getCart(userId),
recClient.getTopRecommendations(userId, 10),
notifClient.getUnreadCount(userId)
).map(t -> new WebDashboardResponse(
t.getT1(), // full user profile object
t.getT2(), // full cart with item list
t.getT3(), // 10 recommendation items
t.getT4() // unread notification count
));
}
}
The Mobile BFF and Web BFF compile and deploy independently. Changing the mobile home screen layout β adding a cart_count field or removing recs β requires a change only to MobileHomeController, with zero negotiation with the web team.
Spring Cloud Gateway as the BFF aggregation layer is an alternative for teams that prefer configuration over code: SCG's AggregateResponseBody filter and request-transformation DSL can implement simple field projection without a dedicated controller class, though complex response shaping still benefits from explicit controller code.
For a full deep-dive on Spring Cloud Gateway aggregation filters and reactive BFF patterns with Spring WebFlux, a dedicated follow-up post is planned.
π What BFF Teaches You About API Design
BFF crystallizes several important API design principles:
- Coupling intent matters. In most architectures, coupling is bad. In BFF, tight coupling between a BFF and its client is intentional β it's what makes the BFF responsive to frontend changes.
- Separation of concerns at the interface layer. Business logic belongs in microservices. Presentation logic (field selection, format conversion) belongs in the BFF.
- Team autonomy scales better than shared APIs. A shared API requires coordination every time any client changes. A BFF-per-team means changes can ship independently.
- You can't optimize for everyone. If one API must serve mobile, web, and IoT devices, it will be suboptimal for all three. Specialization beats generalization when contexts diverge enough.
π TLDR: Summary & Key Takeaways
- BFF creates one dedicated API server per client type instead of one shared API for all.
- Primary jobs: request aggregation, field projection, format translation, and protocol bridging.
- BFF should contain no business logic β it is an orchestration layer, not a domain layer.
- Use it when you have multiple clients with significantly different data needs.
- Avoid it for single-client apps or when it would duplicate logic across BFFs.
π Related Posts
Test Your Knowledge
Ready to test what you just learned?
AI will generate 4 questions based on this article's content.

Written by
Abstract Algorithms
@abstractalgorithms
More Posts
NoSQL Partitioning: How Cassandra, DynamoDB, and MongoDB Split Data
TLDR: Every NoSQL database hides a partitioning engine behind a deceptively simple API. Cassandra uses a consistent hashing ring where a Murmur3 hash of your partition key selects a node β virtual nodes (vnodes) make rebalancing smooth. DynamoDB mana...
Clock Skew and Causality Violations: Why Distributed Clocks Lie
TLDR: Physical clocks on distributed machines cannot be perfectly synchronized. NTP keeps them within tens to hundreds of milliseconds in normal conditions β but under load, across datacenters, or after a VM pause, the drift can reach seconds. When s...
Stale Reads and Cascading Failures in Distributed Systems
TLDR: Stale reads return superseded data from replicas that haven't yet applied the latest write. Cascading failures turn one overloaded node into a cluster-wide collapse through retry storms and redistributed load. Both are preventable β stale reads...
Split Brain Explained: When Two Nodes Both Think They Are Leader
TLDR: Split brain happens when a network partition causes two nodes to simultaneously believe they are the leader β each accepting writes the other never sees. Prevent it with quorum consensus (at least βN/2β+1 nodes must agree before leadership is g...
