Backend for Frontend (BFF) pattern
In this series (15 parts)
- Backend system design scope
- Designing RESTful APIs
- Authentication and session management
- Database design for backend systems
- Caching in backend systems
- Background jobs and task queues
- File upload and storage
- Search integration
- Email and notification delivery
- Webhooks: design and security
- Payments integration
- Multi-tenancy patterns
- Backend for Frontend (BFF) pattern
- GraphQL server design
- gRPC and internal service APIs
A mobile app, a web SPA, and a third-party API integration walk into the same REST endpoint. The mobile app needs a compact payload with minimal images. The web app needs rich, nested data for a dashboard. The API consumer needs stable, versioned fields with no UI-specific cruft. You cannot serve all three well from a single endpoint without the code turning into a mess of conditional logic.
The Backend for Frontend (BFF) pattern solves this by placing a thin, client-specific backend between each client type and your core services. Each BFF aggregates, transforms, and shapes data for exactly one consumer. For context on how services communicate behind the BFF, see microservice communication. For API contract design, see API design.
The problem with one API for all clients
Consider a product page. The web app shows a hero image, reviews, related products, seller info, and a price chart. The mobile app shows a thumbnail, the price, and a “Buy” button. The API integration only needs the product ID, name, price, and stock status.
With a single API, you have three bad options:
- Over-fetch: return everything. Mobile clients download data they never render, wasting bandwidth and battery. API consumers get unstable fields that change when the web team redesigns a page.
- Sparse fieldsets: add
?fields=name,price,stockquery parameters. Now your backend is a generic query engine that is hard to optimize, hard to cache, and hard to document. - Client-specific conditionals: branch inside your handlers based on a
User-AgentorX-Client-Typeheader. This turns every endpoint into a switch statement that grows with every new client.
None of these scale. The BFF pattern does.
BFF architecture
flowchart TB
subgraph Clients
Web["Web App"]
Mobile["Mobile App"]
ThirdParty["3rd Party API"]
end
subgraph BFFs["Backend for Frontend Layer"]
WebBFF["Web BFF"]
MobileBFF["Mobile BFF"]
APIGW["Public API Gateway"]
end
subgraph Services["Core Services"]
ProductSvc["Product Service"]
ReviewSvc["Review Service"]
PricingSvc["Pricing Service"]
UserSvc["User Service"]
end
Web --> WebBFF
Mobile --> MobileBFF
ThirdParty --> APIGW
WebBFF --> ProductSvc
WebBFF --> ReviewSvc
WebBFF --> PricingSvc
WebBFF --> UserSvc
MobileBFF --> ProductSvc
MobileBFF --> PricingSvc
APIGW --> ProductSvc
APIGW --> PricingSvc
BFF architecture. Each client type has a dedicated backend that calls only the core services it needs. The web BFF aggregates four services; the mobile BFF calls only two.
Each BFF is a lightweight service. It does not contain business logic. Its job is:
- Aggregation: call multiple core services and merge the results.
- Transformation: reshape the data for the client’s needs (rename fields, flatten nested objects, resize image URLs).
- Optimization: implement client-specific caching, compression, and pagination strategies.
Single API architecture (for contrast)
flowchart TB
subgraph Clients2
Web2["Web App"]
Mobile2["Mobile App"]
ThirdParty2["3rd Party API"]
end
API["Monolithic API"]
subgraph Services2["Core Services"]
ProductSvc2["Product Service"]
ReviewSvc2["Review Service"]
PricingSvc2["Pricing Service"]
UserSvc2["User Service"]
end
Web2 --> API
Mobile2 --> API
ThirdParty2 --> API
API --> ProductSvc2
API --> ReviewSvc2
API --> PricingSvc2
API --> UserSvc2
Single API architecture. All clients hit the same endpoints. The API must serve every client’s needs, leading to bloated responses and conditional logic.
What a BFF does
Aggregation
A web dashboard page might need data from five services. Without a BFF, the client makes five requests, each with its own latency and failure mode. The BFF makes those calls server-side (where latency between services is measured in single-digit milliseconds within the same datacenter) and returns a single response.
// Web BFF: GET /dashboard
async function getDashboard(userId) {
const [profile, projects, notifications, billing, analytics] =
await Promise.all([
userService.getProfile(userId),
projectService.listRecent(userId, { limit: 5 }),
notificationService.getUnread(userId),
billingService.getCurrentPlan(userId),
analyticsService.getSummary(userId, { period: "7d" })
]);
return {
user: { name: profile.name, avatar: profile.avatarUrl },
recentProjects: projects.map(p => ({ id: p.id, name: p.name })),
unreadCount: notifications.length,
plan: billing.planName,
weeklyStats: analytics.summary
};
}
The mobile BFF for the same user might skip analytics and billing, returning only the profile and recent projects.
Transformation
Different clients need different shapes. The web app uses camelCase. The mobile SDK expects snake_case. The third-party API uses a versioned schema with strict backward compatibility. Each BFF transforms the core service response into the client’s expected format without forcing the core services to support multiple serialization formats.
Client-specific caching
A mobile app hitting the product catalog might benefit from aggressive caching with Cache-Control: max-age=300 because users expect slightly stale data in exchange for speed. The web app might use Cache-Control: no-cache with ETags for the same data because the dashboard should reflect recent changes. The BFF sets these headers per client.
BFF vs API gateway
These patterns overlap but serve different purposes.
| Concern | API Gateway | BFF |
|---|---|---|
| Primary role | Cross-cutting concerns (auth, rate limiting, routing) | Client-specific aggregation and transformation |
| Number of instances | One (or one per region) | One per client type |
| Business logic | None | Light transformation, no domain logic |
| Owned by | Platform/infrastructure team | Client team (web team owns web BFF) |
| Typical tech | Kong, Envoy, AWS API Gateway | Node.js, Go, or whatever the client team prefers |
In practice, you often use both: the API gateway handles authentication, rate limiting, and TLS termination at the edge, then routes requests to the appropriate BFF.
API gateways own cross-cutting infrastructure concerns. BFFs own client-specific data shaping. They complement each other.
Ownership model
The most important organizational decision: who owns each BFF?
Client team owns their BFF. The web team builds and deploys the web BFF. The mobile team builds and deploys the mobile BFF. This is the recommended approach because:
- The team that knows the client’s needs best makes the decisions about data shaping.
- Changes to the web UI do not require coordinating with a backend team. The web team updates their BFF to match their new requirements.
- Deployment cadence matches the client. Mobile ships weekly; the mobile BFF ships weekly. Web ships daily; the web BFF ships daily.
The risk is duplication. Both BFFs might independently implement similar aggregation logic. Accept this. A little duplication between BFFs is far cheaper than the coordination overhead of a shared monolithic API.
When not to use BFF
BFFs add a network hop and a service to maintain. Do not introduce them when:
- You have one client type. If your product is a web app with no mobile app and no public API, a BFF adds complexity without benefit.
- Your API is simple. If most endpoints return a single resource with no aggregation, a BFF is overhead.
- You are a small team. Two engineers should not maintain three BFFs. Start with a single API and extract BFFs when the pain of serving multiple clients becomes concrete.
The signal to introduce a BFF is when you find yourself adding if (client === 'mobile') branches in your API handlers for the third time.
Error handling in BFFs
The BFF calls multiple services. Some will fail. Your error strategy must be explicit:
- Partial responses. If the review service is down but the product service is up, return the product data with a null reviews field and a warning header. Do not fail the entire request because one non-critical service is unavailable.
- Timeouts. Set aggressive timeouts per upstream call. A mobile BFF should not wait 10 seconds for a slow analytics service. Use circuit breakers to stop calling consistently failing services.
- Fallbacks. For non-critical data (recommendations, analytics), return cached data or an empty state. For critical data (product details for a checkout page), fail the request and let the client show an error.
Performance patterns
Parallel calls
Always call independent services in parallel. The response time of a BFF endpoint is the latency of the slowest upstream call, not the sum of all calls. This is the single biggest performance win.
Response compression
Mobile clients on cellular networks benefit enormously from gzip or brotli compression. The web BFF might skip compression for internal dashboard calls over a fast network. Each BFF sets its compression policy based on its client’s typical network conditions.
Field selection at the BFF level
The BFF only requests the fields it needs from core services. If the product service returns 50 fields but the mobile BFF only needs 5, the BFF should call a lightweight endpoint or use field selection (if the core service supports it). This reduces serialization cost and network transfer.
Testing BFFs
BFFs are integration-heavy. Your testing strategy should reflect that:
- Contract tests: verify that the BFF’s expectations of core service responses match the actual contracts. Use tools like Pact.
- Snapshot tests: capture the BFF’s response shape and diff against it. This catches unintended changes to the client contract.
- Failure mode tests: simulate upstream service failures and verify the BFF returns graceful degraded responses.
- Performance tests: measure BFF response time under load. The BFF should add no more than 5 to 15 ms of overhead on top of upstream call latency.
What comes next
The BFF pattern works well when your clients need different data shapes. But what if the client wants to define exactly what data it receives? The next article covers GraphQL server design: schema design, the N+1 problem, authorization at the resolver level, and when GraphQL is the wrong choice.