Backend for Frontend (BFF) Pattern
Give each client type its own backend that aggregates downstream services and shapes data to that platform's needs.
Overview
The Backend for Frontend (BFF) Pattern gives every client type its own backend: rather than forcing web, mobile, and IoT onto one general-purpose API, each frontend talks to a service that aggregates the downstream microservices, reshapes the data, handles auth, and returns exactly what that client needs.
A mobile app wants lightweight, aggregated responses to save battery and bandwidth. A web dashboard wants deeply nested data with real-time updates. An IoT device might only tolerate tiny binary payloads. One shared API can’t serve all three well.
The BFF sits between the client and the core microservices — part aggregator, part transformer, part cache, all tuned to one client type.
When to Use
- For alternatives, see GraphQL vs REST — When to Choose and How to Migrate.
Use the BFF Pattern when:
- Different frontend clients have incompatible data requirements
- Mobile clients need reduced payload size and fewer network round-trips
- Web clients need server-side rendering or SEO-optimized data
- Third-party integrations need a sanitized, limited API surface
- You want to iterate on frontend APIs without affecting other clients
When to Avoid
- You have only one frontend client type (a single API gateway suffices)
- The overhead of maintaining multiple BFF services exceeds the benefit
- Frontend needs are nearly identical across platforms
- The BFF becomes a thick layer containing business logic that belongs in the domain
Solution
Python
from dataclasses import dataclass, asdict
from typing import List
import json
# ============================================================================
# CORE MICROSERVICES (shared backend services)
# ============================================================================
class UserService:
def get_user(self, user_id: str) -> dict:
return {
"id": user_id,
"name": "Alice Johnson",
"email": "alice@example.com",
"address": {"street": "123 Main St", "city": "NYC", "zip": "10001"},
"preferences": {"theme": "dark", "notifications": True},
"created_at": "2022-01-15T00:00:00Z"
}
class OrderService:
def get_orders(self, user_id: str) -> List[dict]:
return [
{"id": "ORD-001", "user_id": user_id, "total": 99.99,
"items": [{"sku": "A1", "name": "Widget", "price": 99.99}],
"status": "delivered", "created_at": "2024-03-01"},
{"id": "ORD-002", "user_id": user_id, "total": 45.00,
"items": [{"sku": "B2", "name": "Gadget", "price": 45.00}],
"status": "shipped", "created_at": "2024-03-10"}
]
class InventoryService:
def get_stock(self, sku: str) -> dict:
return {"sku": sku, "quantity": 150, "warehouse": "US-East"}
# ============================================================================
# BFF FOR MOBILE CLIENT
# ============================================================================
@dataclass
class MobileOrderSummary:
order_id: str
total: float
item_count: int
status: str
@dataclass
class MobileUserProfile:
name: str
recent_orders: List[MobileOrderSummary]
unread_notifications: int
class MobileBFF:
"""BFF optimized for mobile: lightweight, aggregated, minimal fields"""
def __init__(self, user_service: UserService, order_service: OrderService):
self.users = user_service
self.orders = order_service
def get_profile(self, user_id: str) -> MobileUserProfile:
user = self.users.get_user(user_id)
orders = self.orders.get_orders(user_id)
recent_orders = [
MobileOrderSummary(
order_id=o["id"],
total=o["total"],
item_count=len(o["items"]),
status=o["status"]
)
for o in orders[:3] # Only last 3 orders for mobile
]
return MobileUserProfile(
name=user["name"],
recent_orders=recent_orders,
unread_notifications=2 # Aggregated from notification service
)
# ============================================================================
# BFF FOR WEB CLIENT
# ============================================================================
class WebBFF:
"""BFF optimized for web: rich data, nested relationships, full history"""
def __init__(self, user_service: UserService, order_service: OrderService,
inventory_service: InventoryService):
self.users = user_service
self.orders = order_service
self.inventory = inventory_service
def get_profile(self, user_id: str) -> dict:
user = self.users.get_user(user_id)
orders = self.orders.get_orders(user_id)
# Enrich orders with inventory data
enriched_orders = []
for order in orders:
enriched_items = []
for item in order["items"]:
stock = self.inventory.get_stock(item["sku"])
enriched_items.append({
**item,
"in_stock": stock["quantity"] > 0,
"warehouse": stock["warehouse"]
})
enriched_orders.append({**order, "items": enriched_items})
return {
"user": {
"id": user["id"],
"name": user["name"],
"email": user["email"],
"member_since": user["created_at"]
},
"orders": enriched_orders,
"order_count": len(orders),
"total_spent": sum(o["total"] for o in orders)
}
# ============================================================================
# USAGE
# ============================================================================
users = UserService()
orders = OrderService()
inventory = InventoryService()
mobile_bff = MobileBFF(users, orders)
web_bff = WebBFF(users, orders, inventory)
mobile_profile = mobile_bff.get_profile("U-123")
print("MOBILE:", json.dumps(asdict(mobile_profile), indent=2))
web_profile = web_bff.get_profile("U-123")
print("WEB:", json.dumps(web_profile, indent=2))
Java
import java.util.*;
import java.util.stream.*;
// Core services
class UserService {
public Map<String, Object> getUser(String userId) {
Map<String, Object> user = new HashMap<>();
user.put("id", userId);
user.put("name", "Alice Johnson");
user.put("email", "alice@example.com");
user.put("createdAt", "2022-01-15T00:00:00Z");
return user;
}
}
class OrderService {
public List<Map<String, Object>> getOrders(String userId) {
List<Map<String, Object>> orders = new ArrayList<>();
Map<String, Object> o1 = new HashMap<>();
o1.put("id", "ORD-001"); o1.put("total", 99.99); o1.put("status", "delivered");
orders.add(o1);
return orders;
}
}
// Mobile BFF
class MobileBFF {
private final UserService users;
private final OrderService orders;
public MobileBFF(UserService users, OrderService orders) {
this.users = users; this.orders = orders;
}
public Map<String, Object> getProfile(String userId) {
Map<String, Object> user = users.getUser(userId);
List<Map<String, Object>> allOrders = orders.getOrders(userId);
List<Map<String, Object>> recent = allOrders.stream()
.limit(3)
.map(o -> {
Map<String, Object> m = new HashMap<>();
m.put("orderId", o.get("id"));
m.put("total", o.get("total"));
m.put("status", o.get("status"));
return m;
})
.collect(Collectors.toList());
Map<String, Object> profile = new HashMap<>();
profile.put("name", user.get("name"));
profile.put("recentOrders", recent);
return profile;
}
}
// Web BFF
class WebBFF {
private final UserService users;
private final OrderService orders;
public WebBFF(UserService users, OrderService orders) {
this.users = users; this.orders = orders;
}
public Map<String, Object> getProfile(String userId) {
Map<String, Object> user = users.getUser(userId);
List<Map<String, Object>> allOrders = orders.getOrders(userId);
Map<String, Object> profile = new HashMap<>();
profile.put("user", user);
profile.put("orders", allOrders);
profile.put("orderCount", allOrders.size());
double total = allOrders.stream()
.mapToDouble(o -> ((Number) o.get("total")).doubleValue())
.sum();
profile.put("totalSpent", total);
return profile;
}
}
// Usage
public class BFFDemo {
public static void main(String[] args) {
UserService users = new UserService();
OrderService orders = new OrderService();
MobileBFF mobile = new MobileBFF(users, orders);
WebBFF web = new WebBFF(users, orders);
System.out.println(mobile.getProfile("U-123"));
System.out.println(web.getProfile("U-123"));
}
}
JavaScript
// Core microservices
class UserService {
async getUser(userId) {
return { id: userId, name: 'Alice Johnson', email: 'alice@example.com', createdAt: '2022-01-15' };
}
}
class OrderService {
async getOrders(userId) {
return [
{ id: 'ORD-001', total: 99.99, status: 'delivered', items: [{ sku: 'A1', name: 'Widget' }] },
{ id: 'ORD-002', total: 45.00, status: 'shipped', items: [{ sku: 'B2', name: 'Gadget' }] }
];
}
}
class InventoryService {
async getStock(sku) {
return { sku, quantity: 150, warehouse: 'US-East' };
}
}
// Mobile BFF
class MobileBFF {
constructor(userService, orderService) {
this.users = userService;
this.orders = orderService;
}
async getProfile(userId) {
const [user, allOrders] = await Promise.all([
this.users.getUser(userId),
this.orders.getOrders(userId)
]);
const recentOrders = allOrders.slice(0, 3).map(o => ({
orderId: o.id,
total: o.total,
status: o.status,
itemCount: o.items.length
}));
return {
name: user.name,
recentOrders,
unreadNotifications: 2
};
}
}
// Web BFF
class WebBFF {
constructor(userService, orderService, inventoryService) {
this.users = userService;
this.orders = orderService;
this.inventory = inventoryService;
}
async getProfile(userId) {
const [user, allOrders] = await Promise.all([
this.users.getUser(userId),
this.orders.getOrders(userId)
]);
const enrichedOrders = await Promise.all(
allOrders.map(async order => {
const enrichedItems = await Promise.all(
order.items.map(async item => {
const stock = await this.inventory.getStock(item.sku);
return { ...item, inStock: stock.quantity > 0, warehouse: stock.warehouse };
})
);
return { ...order, items: enrichedItems };
})
);
return {
user,
orders: enrichedOrders,
orderCount: allOrders.length,
totalSpent: allOrders.reduce((sum, o) => sum + o.total, 0)
};
}
}
// Usage
async function demo() {
const users = new UserService();
const orders = new OrderService();
const inventory = new InventoryService();
const mobile = new MobileBFF(users, orders);
const web = new WebBFF(users, orders, inventory);
console.log('Mobile:', await mobile.getProfile('U-123'));
console.log('Web:', await web.getProfile('U-123'));
}
demo().catch(console.error);
Explanation
The BFF is an API facade per client type:
- Aggregation: The BFF calls multiple downstream services and assembles a unified response
- Transformation: Data is reshaped to match the client’s needs (mobile gets summaries, web gets full objects)
- Optimization: Payload size, caching, and field selection are tuned per platform
- Isolation: Changes to the web UI don’t affect mobile clients and vice versa
Each BFF is owned by the team responsible for that frontend, allowing them to iterate independently. Unlike a shared entry point such as the Gateway Routing Pattern or a general-purpose Facade, a BFF answers to a single client experience — Sam Newman documented the pattern in Backends For Frontends.
Variants
| Variant | Client Type | Specialization |
|---|---|---|
| Mobile BFF | iOS/Android | Small payloads, connection resilience, offline support |
| Web BFF | Browser/SPA | SSR, SEO, deep data nesting, real-time capabilities |
| IoT BFF | Embedded devices | Binary protocols, minimal JSON, edge caching |
| Partner BFF | Third-party integrations | Rate limiting, restricted data, API versioning |
| GraphQL BFF | Any | Schema stitching, resolver-based aggregation |
GraphQL BFF
When clients differ mainly in field selection, a single GraphQL BFF can serve them all:
const { ApolloServer } = require("@apollo/server");
const { startStandaloneServer } = require("@apollo/server/standalone");
const axios = require("axios");
const typeDefs = `#graphql
type Dashboard { user: User!, recentOrders: [Order!]! }
type User { id: ID!, name: String!, avatarUrl: String }
type Order { id: ID!, status: String!, total: Float! }
type Query { dashboard(userId: ID!): Dashboard! }
`;
const resolvers = {
Query: {
async dashboard(_, { userId }) {
const [user, orders] = await Promise.all([
axios.get(`http://user-service:3001/users/${userId}`),
axios.get(`http://order-service:3002/users/${userId}/orders?limit=5`)
]);
return { user: user.data, recentOrders: orders.data.orders };
}
}
};
startStandaloneServer(new ApolloServer({ typeDefs, resolvers }), { listen: { port: 4000 } });
BFF with caching
Dashboard-style aggregations change slowly — cache them at the BFF for 30–60 s:
const redis = require("redis");
const express = require("express");
const axios = require("axios");
const app = express();
const cache = redis.createClient({ url: process.env.REDIS_URL });
await cache.connect();
app.get("/api/dashboard", async (req, res) => {
const key = `dashboard:${req.headers["x-user-id"]}`;
const cached = await cache.get(key).catch(() => null);
if (cached) return res.json(JSON.parse(cached));
const [user, orders] = await Promise.all([
axios.get(`http://user-service:3001/users/${req.headers["x-user-id"]}`),
axios.get(`http://order-service:3002/users/${req.headers["x-user-id"]}/orders?limit=5`)
]);
const body = { user: user.data, recentOrders: orders.data.orders };
await cache.setEx(key, 60, JSON.stringify(body)).catch(() => {});
res.json(body);
});
Other variants worth knowing: a WebSocket BFF pushes live updates (order status, inventory) to subscribed clients instead of polling, and a shared BFF with per-client-type response shapers works when one deployable is enough but payloads still diverge. For push-pull flow control inside the BFF itself, see the Back-Pressure Pattern.
What Works
- Keep BFFs thin. They should aggregate and transform, not implement core business logic.
- Co-locate BFF with frontend team. The team that owns the client should own its BFF.
- Use GraphQL when clients need flexible queries. A single GraphQL BFF can replace multiple REST BFFs.
- Cache aggressively at the BFF. Reduce downstream service calls with TTL-based caching.
- Share code via libraries. Common HTTP clients, auth middleware, and logging should be library code.
Common Mistakes
- Duplicating business logic. The BFF shouldn’t reimplement validation or calculation rules.
- Over-fetching from downstream. Use targeted service calls, not “fetch everything and filter.”
- One BFF for all clients. This defeats the purpose and creates the same coupling a general API has.
- Not handling failures gracefully. One downstream failure shouldn’t break the entire BFF response.
- Ignoring authentication context. BFFs still need to pass user identity and permissions downstream.
Real-World Examples
Netflix
Netflix runs a dedicated edge service per client experience: its Android team replaced the app’s API backend with a purpose-built endpoint they fully control — documented in the AWS write-up of the BFF pattern.
SoundCloud
The pattern was coined at SoundCloud: Phil Calçado described it in The Back-end for Front-end Pattern, and the company still runs dozens of BFFs — Mobile API, Web API, Public and Partner APIs — handling hundreds of millions of requests per hour, per their Backstage engineering blog.
Origin of the pattern
Sam Newman formalized BFF after observing it at REA and SoundCloud, and his pattern write-up remains the reference: one server-side backend per user experience, owned by the team that ships the client.
Frequently Asked Questions
What is the difference between BFF and API Gateway?
A gateway is the shared front door — routing, auth, rate limits for everyone. A BFF is a backend built for one client type, and a gateway can sit in front of several of them.
Can one BFF serve multiple similar clients?
Sure — iOS and Android often share one. Just don't generalize too early: the whole point of the pattern is client-specific shaping.
Should BFFs handle authentication?
They usually validate tokens and forward identity downstream; the identity provider itself (OAuth, OIDC) stays a shared service.
How do BFFs relate to microservices?
BFFs sit above the microservice layer as consumers of it — they don't replace it.
Should iOS and Android get separate BFFs?
Not automatically. SoundCloud runs one mobile BFF for both apps; REA split per platform. Keep them shared while the apps want the same data.
Where does shared logic live?
In the domain services or shared libraries — anywhere but the BFF. Keep it thin: aggregation, shaping, caching, edge concerns like auth propagation and rate limiting. Never reimplemented business rules.
Does GraphQL replace the BFF?
A GraphQL gateway can act as a single BFF when clients differ mainly in field selection. If they need different caching, auth flows, or release cadence, separate BFFs still help — and you can build each one on GraphQL.
Related Resources
Facade Pattern
Provide a simplified interface to a complex subsystem. A structural pattern that hides implementation details behind a clean API.
PatternAbstract Factory for Cross-Platform UI Component Families
Create families of related objects without specifying concrete classes, enabling platform-specific implementations that share a common interface.
PatternGraphQL Mutation Validation: Centralized Input Checks
Centralize GraphQL mutation validation with reusable rules, custom scalars, and structured errors. Includes TypeScript examples and best practices.
PatternGateway Routing Pattern
Route requests to multiple backend services through a single entry point that handles cross-cutting concerns.
PatternMediator Pattern for Decoupled UI Components
Reduce chaotic dependencies between UI components by introducing a mediator that centralizes communication, preventing explicit references between peers
PatternStatic Content Hosting Pattern
Deploy static files to a dedicated content delivery network or object storage to offload origin servers, reduce latency, and improve availability for assets like images, CSS, and JavaScript.