Guide

Backend for Frontend (BFF) pattern explained

Harbor Commerce's mobile checkout screen needed product title, thumbnail, inventory count, applied coupon, shipping estimate, and tax total in one JSON payload under 8 KB. The web admin dashboard needed the same underlying data spread across twelve paginated tables with audit metadata. Shipping one “generic” order API forced mobile clients to fire six round trips over spotty LTE and parse fields they never displayed. The platform team introduced a Backend for Frontend (BFF) layer — a thin server owned by each client team that aggregates microservice calls, shapes responses for a specific UI, and holds session state the browser or app needs. Sam Newman popularized the term in microservices literature; in practice it is the pattern behind many mobile gateways, Next.js server routes, and partner-specific integration endpoints. This guide explains what a BFF does, how it differs from an API gateway and from GraphQL, when to adopt one BFF per client versus shared layers, a Harbor Commerce mobile checkout worked example, an architecture decision table, common pitfalls, and a production checklist.

What a BFF is (and is not)

A Backend for Frontend is a server-side component that sits between a specific client application (iOS app, React SPA, partner webhook consumer) and your domain microservices. Its job is client-specific orchestration: call the right downstream services, merge results, strip unused fields, apply presentation rules, and return a payload optimized for that one UI.

A BFF is not a second monolith. It should contain no core business rules that belong in domain services — pricing logic, inventory reservation, and payment capture stay in catalog, warehouse, and payments services. The BFF translates and aggregates; it does not become the system of record.

Typical responsibilities

  • Response aggregation — fan out to catalog, cart, pricing, and shipping services in parallel, then return one composite DTO.
  • Field shaping — mobile gets compact nested JSON; admin gets verbose records with foreign-key IDs for editing.
  • Protocol translation — expose REST/JSON to the browser while calling internal gRPC or message queues downstream.
  • Auth session handling — store refresh tokens server-side, attach service-to-service credentials, never expose internal API keys to the client.
  • Resilience at the UI boundary — timeouts, partial fallbacks, and cached stubs so a slow recommendations service does not blank the whole checkout page.

BFF vs API gateway vs GraphQL

Teams conflate these three layers constantly. They solve overlapping problems but at different abstraction levels:

Layer Primary owner Typical scope
API gateway Platform / infra TLS termination, routing, rate limits, JWT validation, cross-cutting policies for all consumers
BFF Client / feature team Per-app aggregation and DTO shaping; knows screen-level data needs
GraphQL server API / domain team Schema where clients pick fields; can act as a shared BFF if one graph serves all clients

In production you often stack them: client → gateway (auth, throttle) → BFF (aggregate) → microservices. GraphQL can replace a BFF when one flexible schema serves web and mobile with acceptable complexity, but resolver N+1 problems and authorization across types still require disciplined engineering. Many teams run a GraphQL BFF for web and a separate REST BFF for mobile push-notification payloads that do not map cleanly to graphs.

One BFF per client vs shared BFF

Newman's original recommendation: one BFF per user experience — separate deployables for iOS, Android, and web when their data shapes diverge materially. Shared BFFs reintroduce the generic-API problem: every new mobile field becomes a negotiation with the web team's release train.

When separate BFFs pay off

  • Mobile needs aggressive payload trimming and offline-friendly bundles.
  • Partner integrations require stable, versioned contracts unrelated to consumer UI churn.
  • Teams are organized around client surfaces (mobile squad ships BFF + app together).

When a shared layer is enough

  • Web and mobile share identical screens and release cadence.
  • Early startup stage: one Next.js or Remix app with server loaders acting as a de facto BFF until traffic justifies splitting.
  • Internal admin tools where one verbose API is acceptable.

Framework choice is secondary: Node/Express, FastAPI, Go, or server components in your meta-framework all work. Pick what the client team already operates.

Implementation patterns

Parallel fan-out with deadlines

A checkout BFF should call catalog, inventory, pricing, and tax services concurrently with per-call timeouts. Sequential chaining turns four 80 ms services into 320 ms perceived latency. Use Promise.allSettled or language equivalents so a non-critical loyalty-points service can fail without blocking purchase completion.

DTO mapping, not entity leaking

Never return internal microservice IDs, Kafka topic names, or raw database rows to the client. Map to stable view models versioned with the UI. When catalog renames sku_internal to product_id, only the BFF mapping changes — mobile binaries do not need an emergency release.

Caching at the BFF edge

Short-TTL caches for product metadata and static CMS blocks cut duplicate fan-out during traffic spikes. Respect cache invalidation events from downstream services; do not cache personalized prices without user-scoped keys. See HTTP caching for Cache-Control semantics at the CDN layer.

Idempotent write orchestration

Checkout BFFs often coordinate multi-step writes (reserve inventory, authorize payment, create shipment). Forward idempotency keys from the client through to each downstream POST so retries do not double-charge. The BFF is the natural place to generate a correlation ID and log the saga steps.

Harbor Commerce mobile checkout: worked example

Harbor Commerce runs catalog, cart, pricing, tax, and shipping as separate services behind an API gateway. The iOS team owned checkout-bff, deployed beside their app release pipeline.

  1. Endpoint: GET /v2/mobile/checkout-summary?cartId=... returns one object: line items with 120×120 thumbnails, subtotal, discounts, estimated delivery window, and tax total.
  2. Fan-out: four parallel gRPC calls with 150 ms deadlines; recommendations omitted on timeout.
  3. Mapping: pricing service returns basis points; BFF converts to locale-formatted currency strings for display only — authoritative amounts stay in pricing service responses stored server-side for the payment step.
  4. Auth: mobile sends a session cookie to the BFF; BFF exchanges it for a short-lived service token via the gateway's token endpoint. Internal service credentials never ship in the app binary.
  5. Observability: one trace span per checkout-summary request with child spans per downstream call; SLO tracked on p95 end-to-end latency under 200 ms.

Result: mobile checkout dropped from six REST calls to one, average payload fell from 42 KB to 6.8 KB, and the iOS team could ship UI experiments without waiting for catalog API versioning committee meetings.

Architecture decision table

Situation Recommended approach Avoid
Single monolith, one web client Server-rendered routes or controllers; no separate BFF service Premature micro-BFF extraction
Microservices, web + mobile with different payloads One BFF per major client surface One “omni” BFF with ?client=mobile flags
Many clients, highly variable field selection GraphQL layer with dataloaders and field-level auth REST endpoints with 40 optional query parameters
Partner B2B integrations Dedicated partner BFF with semver API and SLA isolation Reusing consumer mobile BFF with breaking changes
High fan-out read screens BFF parallel aggregation + edge cache Chatty browser calling microservices directly
Small team, early product Meta-framework server actions as BFF until pain appears Three BFF deployables before product-market fit

Common pitfalls

  • BFF becomes the monolith — business rules creep in because “it is faster here.” Code review should reject domain logic that is not presentation or orchestration.
  • Shared BFF coupling — web and mobile share one deployable; every mobile tweak risks web regressions. Split when PR friction exceeds one team's tolerance.
  • Sequential fan-out — naive await chains multiply latency. Always parallelize independent downstream reads.
  • Leaking internal errors — returning raw 500 bodies from inventory service to mobile users exposes stack traces. Map to stable client error codes.
  • No contract tests — BFF DTOs drift from UI expectations silently. Snapshot or consumer-driven contract tests in CI.
  • Bypassing the gateway — BFF calling microservices on private networks without rate limits or auth creates a shadow attack surface.

Production checklist

  • Document which team owns each BFF and its client version mapping.
  • Enforce parallel downstream calls with per-service timeouts and bulkheads.
  • Propagate trace context and a single correlation ID through fan-out.
  • Version public BFF routes; deprecate with Sunset headers for partners.
  • Keep domain logic in microservices; limit BFF to orchestration and mapping.
  • Add contract tests between BFF responses and client fixtures.
  • Monitor p95/p99 BFF latency and per-downstream error budgets separately.
  • Review quarterly whether GraphQL or a shared schema could collapse redundant BFFs.

Key takeaways

  • A BFF optimizes for one client — not for all consumers at once.
  • Gateways enforce policy; BFFs shape experience — use both layers.
  • Aggregation kills chatty mobile apps — parallel fan-out is non-negotiable.
  • GraphQL is an alternative, not a replacement — choose based on client diversity and team skills.
  • Guard against the stealth monolith — orchestration only, domain logic downstream.

Related reading