# 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.

- kind: pattern
- category: architecture
- aliases: BFF
- canonical: https://aipatterns.dev/design-patterns/backends-for-frontends
- inspired-by: https://learn.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends

## 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](https://aipatterns.dev/design-patterns/facade.md) 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

```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 --> PricingService
```

### Participants

- **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 `fields` parameter 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

1. Split per client only where needs actually differ; start with the client whose pain is loudest and keep the shared API for the rest.
2. 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.
3. Shape views for the screen, not the entities - `WebProductView` joins product, price, and reviews because the page needs all three, not because the domain does.
4. Keep business rules out. The BFF may default, trim, and reorder; it may not decide anything the services would disagree with.
5. 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.
6. Watch for composition patterns repeating across BFFs; the second time two BFFs write the same aggregation, promote it into a service.
7. 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.

```ts title="bff.ts"
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 }
```

```python title="bff.py"
from dataclasses import dataclass

@dataclass
class Product:
    id: str
    name: str
    description: str

@dataclass
class Price:
    cents: int

@dataclass
class Review:
    author: str
    stars: int

# Shared services: they know no client.
class CatalogService:
    def product(self, id: str) -> Product:
        return Product(id, "Oolong", "oxidized 60%")

class PricingService:
    def price(self, id: str) -> Price:
        return Price(900)

class ReviewService:
    def reviews(self, id: str) -> list[Review]:
        return [Review("sam", 5)]

class WebBff:
    """Web BFF: one call, full page shape."""

    def __init__(self, catalog: CatalogService, pricing: PricingService, reviews: ReviewService):
        self._catalog, self._pricing, self._reviews = catalog, pricing, reviews

    def product_page(self, id: str) -> dict:
        product = self._catalog.product(id)
        price = self._pricing.price(id)
        review_list = self._reviews.reviews(id)
        return {
            **vars(product),
            "price_cents": price.cents,
            "reviews": [vars(r) for r in review_list],
        }

class MobileBff:
    """Mobile BFF: one call, compact shape, no reviews on the list screen."""

    def __init__(self, catalog: CatalogService, pricing: PricingService):
        self._catalog, self._pricing = catalog, pricing

    def product_summary(self, id: str) -> dict:
        product = self._catalog.product(id)
        price = self._pricing.price(id)
        return {"id": product.id, "name": product.name, "price_cents": price.cents}

catalog, pricing, reviews = CatalogService(), PricingService(), ReviewService()
web = WebBff(catalog, pricing, reviews)
mobile = MobileBff(catalog, pricing)

print(web.product_page("tea-1"))
print(mobile.product_summary("tea-1"))  # {'id': 'tea-1', 'name': 'Oolong', 'price_cents': 900}
```

```go title="bff.go"
package main

import "fmt"

type Product struct{ ID, Name, Description string }
type Price struct{ Cents int }
type Review struct{ Author string; Stars int }

// Shared services: they know no client.
type CatalogService struct{}
type PricingService struct{}
type ReviewService struct{}

func (CatalogService) Product(id string) Product {
	return Product{ID: id, Name: "Oolong", Description: "oxidized 60%"}
}
func (PricingService) Price(id string) Price      { return Price{Cents: 900} }
func (ReviewService) Reviews(id string) []Review  { return []Review{{"sam", 5}} }

// WebBFF: one call, full page shape.
type WebBFF struct {
	catalog CatalogService
	pricing PricingService
	reviews ReviewService
}

type webProductView struct {
	ID          string
	Name        string
	Description string
	PriceCents  int
	Reviews     []Review
}

func (b WebBFF) ProductPage(id string) webProductView {
	p := b.catalog.Product(id)
	return webProductView{p.ID, p.Name, p.Description, b.pricing.Price(id).Cents, b.reviews.Reviews(id)}
}

// MobileBFF: one call, compact shape, no reviews on the list screen.
type MobileBFF struct {
	catalog CatalogService
	pricing PricingService
}

type mobileProductView struct {
	ID         string
	Name       string
	PriceCents int
}

func (b MobileBFF) ProductSummary(id string) mobileProductView {
	p := b.catalog.Product(id)
	return mobileProductView{p.ID, p.Name, b.pricing.Price(id).Cents}
}

func main() {
	web := WebBFF{CatalogService{}, PricingService{}, ReviewService{}}
	mobile := MobileBFF{CatalogService{}, PricingService{}}

	fmt.Printf("%+v\n", web.ProductPage("tea-1"))
	fmt.Printf("%+v\n", mobile.ProductSummary("tea-1"))
}
```

```java title="Bff.java"
import java.util.List;

public class Bff {
    record Product(String id, String name, String description) {}

    record Price(int cents) {}

    record Review(String author, int stars) {}

    // Shared services: they know no client.
    static final class CatalogService {
        Product product(String id) {
            return new Product(id, "Oolong", "oxidized 60%");
        }
    }

    static final class PricingService {
        Price price(String id) {
            return new Price(900);
        }
    }

    static final class ReviewService {
        List<Review> reviews(String id) {
            return List.of(new Review("sam", 5));
        }
    }

    /** Web BFF: one call, full page shape. */
    record WebBff(CatalogService catalog, PricingService pricing, ReviewService reviews) {
        Map<String, Object> productPage(String id) {
            Product p = catalog.product(id);
            return Map.of(
                    "id", p.id(), "name", p.name(), "description", p.description(),
                    "priceCents", pricing.price(id).cents(), "reviews", reviews.reviews(id));
        }
    }

    /** Mobile BFF: one call, compact shape, no reviews on the list screen. */
    record MobileBff(CatalogService catalog, PricingService pricing) {
        Map<String, Object> productSummary(String id) {
            Product p = catalog.product(id);
            return Map.of("id", p.id(), "name", p.name(), "priceCents", pricing.price(id).cents());
        }
    }

    public static void main(String[] args) {
        var web = new WebBff(new CatalogService(), new PricingService(), new ReviewService());
        var mobile = new MobileBff(new CatalogService(), new PricingService());

        System.out.println(web.productPage("tea-1"));
        System.out.println(mobile.productSummary("tea-1"));
    }
}
```

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](https://aipatterns.dev/design-patterns/facade.md): each BFF is a facade over the services - but with a single, known, owned consumer, which is what keeps it honest.
- [Gateway Aggregation](https://learn.microsoft.com/en-us/azure/architecture/patterns/gateway-aggregation): the same join-many-calls move without the per-client split; reach for it when one aggregation serves everyone.
- [Adapter](https://aipatterns.dev/design-patterns/adapter.md): 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.
