Design patternArchitecture Patterns
Backends for Frontends
Give each frontend its own backend that aggregates and shapes the services it needs - so the web app, the mobile app, and any other client stop negotiating one API into serving them all.
- Complexity
- Medium
- Seen in the wild
- Medium
- Also known as
- BFF
Intent
When several frontends consume one shared backend, every new client need changes an API everyone depends on. A backend for frontends (BFF) splits that backend per client: each frontend gets its own aggregation layer that calls the downstream services, composes them, and returns exactly the shape, payload, and protocol that client wants. The shared services stay business-owned; the per-client shape moves to a layer that only that client's team can change.
Forces
- One API vs many clients: web wants full objects, mobile wants compact payloads and few round trips, a kiosk wants three fields - one shape serves none of them well.
- Autonomy vs duplication: each BFF is independently deployable and owned, and each re-implements some composition the others also do.
- Downstream purity vs client convenience: shaping logic must live somewhere; in the services it multiplies per client, in the client it bloats frontends with backend knowledge.
- Team boundaries vs release coupling: a shared backend couples every frontend's release to every other's; per-client backends decouple them at the price of more deployments.
Therefore
Put one backend layer per frontend, between that client and the shared services: it calls the services, aggregates what the screen needs, and returns the client's exact shape - and nothing downstream knows any client exists.
The BFF is a Facade with an owner: one per client, not one for the world. It may cache, trim, join, and re-protocol (REST to the web, gRPC to mobile), but it holds no business rules - invariants stay in the services. Where two BFFs keep re-implementing the same composition, that composition belongs in a service.
Structure
Diagram source (Mermaid)
classDiagram
class WebBff {
+productPage(id) WebProductView
}
class MobileBff {
+productSummary(id) MobileProductView
}
class CatalogService {
+product(id) Product
}
class PricingService {
+price(id) Price
}
class ReviewService {
+reviews(id) Review[]
}
WebBff --> CatalogService
WebBff --> PricingService
WebBff --> ReviewService
MobileBff --> CatalogService
MobileBff --> PricingServiceParticipants
- Client BFFs (
WebBff,MobileBff): one per frontend. Aggregate the services, shape the payload, own that client's caching and protocol. No invariants. - Shared services (
CatalogService,PricingService,ReviewService): the business. Knows no client, returns domain data, enforces the rules. - Views (
WebProductView,MobileProductView): the per-client shapes - full page data for web, a compact summary for mobile.
Use it when
- Two or more frontends with materially different needs consume the same services, and the shared API has become the meeting point of every roadmap.
- Frontend teams deploy independently and a shared backend's release train is the bottleneck.
- Clients have different transport or latency constraints - mobile needs one round trip and small payloads; desktop tolerates chatty calls for freshness.
- A specific client needs aggressive composition - many small calls joined into one screen - that would otherwise leak into the services.
Do not use it when
- There is one frontend. A BFF for one client is a layer with no one to arbitrate between; it is just a slower way to write the API.
- The clients' needs are genuinely identical - one well-designed API serves them; two BFFs would be copies that drift.
- The team is too small to own the extra deployables - a BFF per client per team multiplies surfaces; a modular monolith endpoint per view achieves the shape without the fleet.
- The difference is just caching or trimming - a CDN, query parameters, or a
fieldsparameter is the fix, not a new backend.
Application grill
Inspect first: the clients sharing one API today and the shape-mangling each performs; the gateway or proxy layers already in front; the deploy targets the platform offers (serverless functions, containers, edge).
Missing pieces: each backend needs its own deploy target. Serverless functions are the cheapest start and scale to zero; a long-lived service earns its keep only when a backend holds connections (WebSockets) or warm SDK clients. An API gateway (Kong, a cloud gateway) can serve simple aggregation without new code - check before writing any.
Ask before you build:
- Scale: what is each client's traffic shape, and must one client's spikes stay isolated from the others?
- Performance: does each client really need one aggregated round trip, or would one new endpoint on the existing API do?
- Reliability: when a downstream service fails, does the backend pass the failure through or serve a degraded view?
- Budget: one client means one deploy, one on-call surface, one auth integration - is running N of those owned?
- Implementation: who translates auth and sessions per client, and where does that logic live?
Implementation steps
- Split per client only where needs actually differ; start with the client whose pain is loudest and keep the shared API for the rest.
- Put the BFF on the server, as close to the services as possible: it aggregates server-side so the client makes one call, and it can hold per-client caches.
- Shape views for the screen, not the entities -
WebProductViewjoins product, price, and reviews because the page needs all three, not because the domain does. - Keep business rules out. The BFF may default, trim, and reorder; it may not decide anything the services would disagree with.
- Define each BFF's ownership: the frontend team owns theirs, the service teams own the services, and the contract between them is the service API - reviewed like any API.
- Watch for composition patterns repeating across BFFs; the second time two BFFs write the same aggregation, promote it into a service.
- Version per client: a BFF can change whenever its client ships, which is the point - never let one client's BFF grow endpoints for another.
Example
A product page for two clients. Web gets the full page in one call - product, price, and reviews joined; mobile gets a compact summary in one call, reviews omitted and price rounded. The services know no client.
type Product = { id: string; name: string; description: string };
type Price = { cents: number };
type Review = { author: string; stars: number };
// Shared services: they know no client.
const catalog = {
product: (id: string): Product => ({ id, name: "Oolong", description: "oxidized 60%" }),
};
const pricing = { price: (id: string): Price => ({ cents: 900 }) };
const reviews = {
reviews: (id: string): Review[] => [{ author: "sam", stars: 5 }],
};
// Web BFF: one call, full page shape.
const webBff = {
productPage(id: string) {
const product = catalog.product(id);
const price = pricing.price(id);
const reviewList = reviews.reviews(id);
return { ...product, priceCents: price.cents, reviewList, rating: 5 };
},
};
// Mobile BFF: one call, compact shape, no reviews on the list screen.
const mobileBff = {
productSummary(id: string) {
const product = catalog.product(id);
const price = pricing.price(id);
return { id: product.id, name: product.name, priceCents: price.cents };
},
};
console.log(webBff.productPage("tea-1"));
// { id, name, description, priceCents: 900, reviewList: [...], rating: 5 }
console.log(mobileBff.productSummary("tea-1"));
// { id: "tea-1", name: "Oolong", priceCents: 900 }When mobile later needs reviews on its detail screen, only MobileBff changes - the web BFF, the services, and every other client stay untouched, which is the autonomy the pattern is buying.
Trade-offs
Gains
- Each client's API evolves on the client's release cadence, owned by the client's team.
- Clients make fewer calls; aggregation happens server-side, close to the services.
- Services stop accumulating per-client flags and shapes and stay domain-shaped.
Costs
- Some aggregation logic is written twice; the second copy is the signal to promote it into a service.
- More deployables, more ownership boundaries, more things to monitor.
- Business rules can leak into a BFF silently - "just this once" becomes a second source of truth.
- For one client or identical clients, the pattern is pure overhead.
Relations
- Facade: each BFF is a facade over the services - but with a single, known, owned consumer, which is what keeps it honest.
- Gateway Aggregation: the same join-many-calls move without the per-client split; reach for it when one aggregation serves everyone.
- Adapter: the shape translation each BFF performs - service interfaces in, client view out.
Agent checklist
- One BFF per client whose needs actually differ; shared API remains for the rest.
- The BFF holds no business rules - defaults and shapes only, invariants stay in services.
- Views are shaped for the screen, not mirrored from entities.
- The same aggregation appearing in two BFFs is promoted into a service, not copied again.
- Ownership of each BFF is named, and its contract with the services is reviewed like any API.
See also
Original writing and examples. Topic coverage follows Microsoft Azure Architecture Center, which explains this topic with its own illustrations.