Search…

Backend for Frontend (BFF) pattern

In this series (15 parts)
  1. Backend system design scope
  2. Designing RESTful APIs
  3. Authentication and session management
  4. Database design for backend systems
  5. Caching in backend systems
  6. Background jobs and task queues
  7. File upload and storage
  8. Search integration
  9. Email and notification delivery
  10. Webhooks: design and security
  11. Payments integration
  12. Multi-tenancy patterns
  13. Backend for Frontend (BFF) pattern
  14. GraphQL server design
  15. 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:

  1. 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.
  2. Sparse fieldsets: add ?fields=name,price,stock query parameters. Now your backend is a generic query engine that is hard to optimize, hard to cache, and hard to document.
  3. Client-specific conditionals: branch inside your handlers based on a User-Agent or X-Client-Type header. 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:

  1. Aggregation: call multiple core services and merge the results.
  2. Transformation: reshape the data for the client’s needs (rename fields, flatten nested objects, resize image URLs).
  3. 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.

ConcernAPI GatewayBFF
Primary roleCross-cutting concerns (auth, rate limiting, routing)Client-specific aggregation and transformation
Number of instancesOne (or one per region)One per client type
Business logicNoneLight transformation, no domain logic
Owned byPlatform/infrastructure teamClient team (web team owns web BFF)
Typical techKong, Envoy, AWS API GatewayNode.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:

  1. 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.
  2. 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.
  3. 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:

  1. Contract tests: verify that the BFF’s expectations of core service responses match the actual contracts. Use tools like Pact.
  2. Snapshot tests: capture the BFF’s response shape and diff against it. This catches unintended changes to the client contract.
  3. Failure mode tests: simulate upstream service failures and verify the BFF returns graceful degraded responses.
  4. 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.

Start typing to search across all content
navigate Enter open Esc close