Backend for Frontend: One API Per Client Type

Series: System Design · Architecture Patterns — Pillar 7 of 8
Systems Design
| # | Post | What it covers |
|---|---|---|
| 00 | Architecture Patterns: How Systems Are Structured | Twenty patterns covering monoliths, microservices, events, resilience, deployment, and data processing. How to structure systems that survive growth. |
| 01 | Monolithic Architecture: The Default That Gets Abandoned Too Early | Monoliths are fast to build and easy to operate. Learn when they're the right choice, when they break down, and how to know the difference. |
| 02 | Microservices: The Architecture You Earn, Not Choose | Microservices enable independent scaling and team autonomy — but at significant cost. Learn what you actually get, what you pay, and when it's worth it. |
| 03 | Serverless: Pay for What You Use, Not What You Provision | Serverless scales to zero and charges per invocation. Learn where it shines, where it fails, and how to design around cold starts and vendor lock-in. |
| 04 | Event-Driven Architecture: Decoupling Through Events | Event-driven systems communicate via events rather than direct calls. Learn how producers, consumers, and event brokers work — and the consistency tradeoffs involved. |
| 05 | Message Queues: Decoupling Produce from Consume | Message queues decouple producers and consumers, enable load levelling, and provide durability. Learn how they work and when to use Kafka vs SQS vs RabbitMQ. |
| 06 | Pub/Sub: Broadcasting Events to Multiple Consumers | Pub/sub decouples publishers from subscribers through topics. Learn how it differs from message queues and when to use Kafka, SNS, or Google Pub/Sub. |
| 07 | CQRS: When Reads and Writes Need Different Models | CQRS separates writes from reads so each can be optimised independently. Learn how it works, when it's worth the complexity, and when it isn't. |
| 08 | Event Sourcing: The Ledger, Not the Balance | Event sourcing stores state as a sequence of events. Learn how it works, what you get (audit log, time travel), and what it costs (complexity, schema evolution). |
| 09 | The Saga Pattern: Distributed Transactions Without Locks | The Saga pattern manages distributed transactions across services using compensating transactions. Learn choreography vs orchestration and when to use each. |
| 10 | The Outbox Pattern: Atomic Writes and Event Publishing | The Outbox pattern solves the dual-write problem — publishing an event and writing to a database atomically. Learn how it works using CDC or polling. |
| 11 | The Circuit Breaker: Stopping Cascading Failures | Circuit breakers prevent cascading failures by fast-failing calls to unhealthy dependencies. Learn the three states, how to configure them, and where to apply them. |
| 12 | The Bulkhead Pattern: Containing Failures Through Resource Isolation | Bulkheads isolate thread pools and connections per dependency so one failure can't exhaust resources needed by others. Learn how to apply them in practice. |
| 13 | The Sidecar Pattern: Cross-Cutting Concerns Without Code Changes | The sidecar pattern deploys a helper process alongside each service for logging, metrics, TLS, and service discovery — without modifying the service itself. |
| 14 | Service Mesh: A Programmable Network for Microservices | A service mesh handles service-to-service traffic, mTLS, circuit breaking, and observability via a fleet of sidecar proxies. Learn how it works and when to use it. |
| 15 | Service Discovery: Finding Services in a Dynamic Environment | Service discovery lets services find each other in dynamic environments. Learn client-side vs server-side discovery, health checks, and DNS vs registry approaches. |
| 16 | The Strangler Fig: Replacing a Legacy System Without Burning It Down | The Strangler Fig replaces a legacy system incrementally by routing specific functionality to new implementations while the old system keeps running. |
| 17 | Backend for Frontend: One API Per Client Type ← you are here | BFF creates dedicated API backends per client type. Learn why one general API struggles to serve mobile and web well, and how BFF solves it. |
| 18 | ETL Pipelines: Moving Data from Operations to Analytics | ETL moves data from operational systems into analytical stores. Learn how pipelines work, what ELT is, and how to design reliable data movement at scale. |
| 19 | Batch vs Stream Processing: How Fresh Do Your Answers Need to Be? | Batch processes accumulate data then processes in bulk; streaming processes each event as it arrives. Learn the tradeoffs and when each is right. |
| 20 | MapReduce: Processing Petabytes in Parallel | MapReduce processes massive datasets in parallel by splitting work into map and reduce phases. Learn how it works and why Spark has largely replaced it. |
| 21 | Architecture Patterns: Wrap-Up | A recap of all 20 architecture patterns across decomposition, async communication, data patterns, resilience, and data processing. How they connect. |
Backend for Frontend: One API Per Client Type
The problem
Your URL shortener has one API. The mobile app and the web dashboard both call the same API.
The mobile app's dashboard screen needs: the user's ten most recent links, each with its total click count. One request, small payload, fast.
The web dashboard needs: the user's links with full metadata — tags, click counts by day for 30 days, country breakdown, device breakdown, custom slug, scheduled expiry, team membership. A much larger payload, multiple data sources, complex aggregation.
Your single API has to serve both. You have two options:
Option A: make the API return everything (the web dashboard's full payload) and let the mobile app ignore most of it. Mobile clients download 10KB where 1KB was enough. Battery and bandwidth are wasted.
Option B: make the API return the minimal payload (the mobile app's needs) and let the web dashboard make multiple requests to assemble what it needs. The dashboard makes 8 API calls per page load. Latency is compounded. Each call has network overhead.
A general-purpose API serving multiple client types always makes one client pay for the other's requirements.
The core idea
The Backend for Frontend (BFF) pattern creates a dedicated API backend for each client type (mobile, web, public API, third-party integrations). Each BFF is optimised for its client's specific data needs — aggregating data from downstream services, shaping the response, and handling client-specific concerns like auth flows, pagination patterns, or response formats.
The analogy: a personal assistant vs a general enquiry desk
A general-purpose API is like a corporate switchboard: it routes your query to the right department and gives you the standard answer. Useful for everyone, optimised for no one.
A BFF is like a personal assistant for each client type: the mobile team's assistant knows exactly what the mobile app needs, fetches it in the right form, and returns a perfect response. The web team's assistant does the same for the web. Each assistant works for their specific client; neither tries to serve everyone.
How it works
The pattern
Mobile App → Mobile BFF → Downstream services
Web Dashboard → Web BFF → (Link Service, Analytics, User Service)
Third-Party API → Public API BFF →
Each BFF is a thin layer owned by the client team. It:
- Aggregates responses from multiple downstream services into one response
- Shapes the payload for its client's UI (include exactly the fields the client needs)
- Handles auth and session management in the way its client expects (mobile: token refresh, web: cookie session)
- Translates between the client's protocol/format and downstream services
Mobile BFF example
@app.get("/v1/dashboard")
async def mobile_dashboard(user_id: str):
# Fetch only what mobile needs — in parallel
links, user = await asyncio.gather(
link_service.get_recent_links(user_id, limit=10),
user_service.get_display_info(user_id)
)
# Shape response for mobile — minimal payload
return {
"user": {"name": user.display_name, "avatar_url": user.avatar_url},
"links": [
{"code": l.short_code, "clicks": l.click_count_total, "url": l.destination}
for l in links
]
}
The mobile BFF makes two parallel calls, aggregates them, and returns a 1KB response.
Web BFF example
@app.get("/v1/dashboard")
async def web_dashboard(user_id: str):
# Web needs much more — fetch all in parallel
links, analytics, user, team = await asyncio.gather(
link_service.get_all_links(user_id),
analytics_service.get_30_day_breakdown(user_id),
user_service.get_full_profile(user_id),
team_service.get_membership(user_id)
)
return {
"user": user,
"team": team,
"links": [enrich(l, analytics) for l in links]
}
The web BFF makes four parallel calls, aggregates them, and returns a 20KB response tailored to the dashboard's needs.
Same downstream services. Two different BFFs. Two different response shapes.
BFF vs API Gateway
| API Gateway | BFF | |
|---|---|---|
| Purpose | Cross-cutting infrastructure: auth, rate limiting, routing | Client-specific data aggregation and shaping |
| Ownership | Platform team | Client team |
| Logic | Minimal (routing, auth, rate limiting) | Business-level (aggregation, field selection, transformation) |
| Count | Usually one | One per client type |
An API Gateway handles the infrastructure layer; BFFs handle client-specific concerns. They work together: the gateway provides the entry point, BFFs sit behind it providing client-specific APIs.
Tradeoffs
Team ownership alignment. The mobile team owns the Mobile BFF — they control its evolution without coordinating with the web team. This is the pattern's primary benefit. The cost: the mobile team must also understand and maintain backend code.
Code duplication. Multiple BFFs calling the same downstream services with slightly different parameters can duplicate logic. Keep shared business logic in downstream services; BFFs only aggregate and shape.
Additional services to operate. Three client types → three BFF deployments → three things to monitor, scale, and maintain. Each BFF is simple, but the total surface area grows.
Not for third-party APIs. A public API serving third parties should be general-purpose and stable — you don't control how third parties use it. BFF is for first-party clients where you control both the frontend and the API.
The one thing to remember
A BFF is a thin aggregation layer owned by the client team, optimised for one client's exact data needs. Instead of a general API that makes every client's experience mediocre, each client gets an API that fits exactly. The mobile app gets a small, fast, mobile-shaped response; the web app gets a rich, aggregated, web-shaped response. The same downstream services power both. The BFF is where client-specific concerns live — response shaping, auth flows, pagination patterns — not in the shared downstream services.
← Previous: Strangler Fig — the safe way to migrate a legacy system incrementally, without a risky big-bang rewrite.
→ Next: ETL Pipelines — moving data from operational systems into analytical stores, transforming it for the queries analysts and data scientists actually run.



