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 AlgorithmsTLDR: 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.
π’ 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.
π 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.
βοΈ 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.
π 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.
π Practice Quiz
What is the core purpose of the Backend for Frontend (BFF) pattern?
- A) To replace all microservices with a single monolithic backend.
- B) To create a dedicated, client-specific API layer that shapes data for each frontend type.
- C) To handle authentication and authorization for all services centrally.
- D) To cache all API responses at the edge CDN. Correct Answer: B β A BFF is a thin orchestration layer tailored to one client type, responsible for request aggregation, field projection, and format translation β not business logic.
Your mobile BFF and web BFF share 75% of their aggregation logic. What should you do?
- A) Merge them into one shared BFF to remove duplication.
- B) Extract shared logic into reusable service clients or an SDK used by both BFFs.
- C) Move the shared logic into the microservices regardless of domain fit.
- D) Accept the duplication; it's unavoidable with BFF. Correct Answer: B β When BFFs share significant logic, extract it into a shared library of service clients. Merging them defeats the purpose of client-specific backends.
A pricing calculation needs to be added to your mobile BFF. What should you do?
- A) Add it to the BFF since it's only needed on mobile.
- B) Reject it β business logic belongs in a dedicated microservice, not in a BFF.
- C) Add it to both the mobile and web BFF to keep them in sync.
- D) Move it to the API gateway so all clients benefit. Correct Answer: B β BFFs should contain no business logic. Pricing rules are domain logic and belong in a service that all clients can share.
When is BFF not the right pattern to use?
- A) When you have both mobile and web clients with different data needs.
- B) When you have a single client type and a simple, uniform API works fine.
- C) When you want each frontend team to own its own backend layer.
- D) When your microservices use different internal protocols. Correct Answer: B β BFF adds operational overhead. For single-client apps or very simple backends, it's unnecessary complexity.
π οΈ 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.
π Related Posts

Written by
Abstract Algorithms
@abstractalgorithms
More Posts

Types of LLM Quantization: By Timing, Scope, and Mapping
TLDR: There is no single "best" LLM quantization. You classify and choose quantization along three axes: when you quantize (timing), what you quantize (scope), and how values are encoded (mapping). In practice, most teams start with weight quantizati...
Stream Processing Pipeline Pattern: Stateful Real-Time Data Products
TLDR: Stream pipelines succeed when event-time semantics, state management, and replay strategy are designed together β and Kafka Streams lets you build all three directly inside your Spring Boot service. Stripe's real-time fraud detection processes...
Service Mesh Pattern: Control Plane, Data Plane, and Zero-Trust Traffic
TLDR: A service mesh intercepts all service-to-service traffic via injected Envoy sidecar proxies, letting a platform team enforce mTLS, retries, timeouts, and circuit breaking centrally β without changing application code. Reach for it when cross-te...
Serverless Architecture Pattern: Event-Driven Scale with Operational Guardrails
TLDR: Serverless is strongest for spiky asynchronous workloads when cold-start, observability, and state boundaries are intentionally designed. TLDR: Serverless works best for spiky, event-driven workloads when you design for idempotency, observabili...
