Home October 17, 2025 5 min read Architecture By Arunkumar Ganesan

BFF Architecture Pattern: Not Every Backend for a UI Is a BFF

A Backend for Frontend is not just a backend called by a frontend. It is a client-specific adaptation layer that lets the user experience move fast without bending core domain services into UI-shaped knots.

One frontend A real BFF is shaped around one client experience, not every consumer.
Not the system of record Business truth still belongs in domain services and durable data stores.
REST or GraphQL The implementation style should match the UI composition problem.

I have seen teams call any backend used by a UI a BFF, and that shortcut creates confusion. I recommend using the term only when the service is deliberately shaped around one client experience and protects domain services from UI specific churn.

Then the product grew. The desktop app wanted tables, filters, bulk actions, and side panels. Mobile wanted smaller payloads, fewer network calls, and flows designed for a narrow screen. Support wanted refund history, fraud flags, shipment events, and internal notes that should never appear in the customer app. The shared API became a compromise layer. Too much data for one client, too little for another, and too much release pressure on one backend team.

The old compromise

Mobile Web app Support UI Shared API one contract tries to fit every UI Orders Payments Shipping mobile waits for web fields support needs internal data one shared API becomes the negotiation table

A BFF solves this by putting an experience-specific backend between the frontend and the domain services. It composes downstream calls, shapes the payload, handles partial failure, carries client-specific authorization, and changes with the frontend release cycle. The order service stays the order service. The payment service stays the payment service. The BFF adapts those capabilities for the screen.

If the React app calls an orders-api, that does not automatically make orders-api a BFF. If that API owns order state, order rules, and order workflows, it is a domain service exposed to a UI.

The split that actually helps

Mobile Web Support Mobile BFF small payload Web BFF page composition Support BFF internal view Orders Payments Shipping Risk each BFF changes with its UI, while domain services stay reusable

What makes it a BFF

When I talk about "What makes it a BFF", I am checking whether BFF Architecture Pattern makes ownership, failure handling, or rollback clearer.

A BFF is defined by ownership and purpose. It is built for one frontend experience. It returns the payload that experience needs. It hides downstream service choreography from the client. It handles screen-specific partial failure. It may cache view data. It may translate external errors into useful UI states. It should not own durable domain state or core business rules.

The practical test is blunt: if removing one frontend would make the service mostly meaningless, it might be a BFF. If five unrelated clients depend on it as a shared contract, it is probably a platform API, gateway, or domain service.

REST BFF works when the workflow is clear

My recommendation in "REST BFF works when the workflow is clear" is to write the operational cost beside the architecture.

REST is a good fit when the frontend has stable screens or explicit workflows. Mobile checkout summary, order cancellation, payment retry, account overview, and support case detail often work well as route-shaped endpoints.

@GetMapping("/mobile/orders/{orderId}/summary")
MobileOrderSummary mobileSummary(@PathVariable String orderId,
                                 AuthenticatedUser user) {
    Order order = orderClient.fetch(orderId, user.customerId());
    Shipment shipment = shipmentClient.latest(orderId)
        .completeOnTimeout(Shipment.unknown(), 250, MILLISECONDS)
        .join();
    PaymentState payment = paymentClient.state(order.paymentId())
        .completeOnTimeout(PaymentState.unavailable(), 250, MILLISECONDS)
        .join();

    boolean showSupport = shipment.isLate() || payment.needsAction();

    return new MobileOrderSummary(
        order.id(),
        order.displayStatus(),
        shipment.etaText(),
        payment.nextActionLabel(),
        showSupport
    );
}

The endpoint is not exposing the order service directly. It builds the exact mobile view, puts time limits around optional downstream calls, and turns partial failure into a safe UI decision instead of making the phone app understand every backend dependency.

GraphQL BFF works when the screen graph moves

What I learnt around "GraphQL BFF works when the screen graph moves" is that a clean diagram is not enough if the failure path is vague.

GraphQL is useful when several screens need different slices of the same connected data: customer, order, shipment, payment, refund, risk, notes, and support actions. The client asks for the fields it needs. The BFF schema becomes the UI contract.

That flexibility needs guardrails. Production GraphQL BFFs need authorization at resolver boundaries, query depth limits, persisted queries, batching, and careful resolver design. Otherwise the BFF becomes an expensive query machine that turns one screen into dozens of hidden downstream calls.

type Order {
  id: ID!
  status: String!
  shipment: Shipment
  payment: PaymentState
}

class OrderResolvers {
  CompletionStage<Shipment> shipment(Order order, DataFetchingEnvironment env) {
    return env.<DataLoader<String, Shipment>>getDataLoader("shipmentByOrderId")
        .load(order.id());
  }

  CompletionStage<PaymentState> payment(Order order, DataFetchingEnvironment env) {
    return env.<DataLoader<String, PaymentState>>getDataLoader("paymentByOrderId")
        .load(order.id());
  }
}

The schema gives the UI a clean order graph, while DataLoader batches downstream calls by order id. That avoids the classic GraphQL resolver mistake where a list of twenty orders becomes forty separate shipment and payment calls.

REST and GraphQL can live together

Use REST when the workflow is command heavy, cache behavior is simple, endpoints are stable, and OpenAPI contracts are enough.
Use GraphQL when the UI is read heavy, screens compose many entities, and clients need different field sets from the same graph.

A common production split is REST for commands and GraphQL for read composition. For example, a mobile app may call REST to cancel an order because that is a clear action, and GraphQL to render the order detail screen because the fields change by product, region, and user state.

What a BFF should not become

I use "What a BFF should not become" to test whether the pattern helps on a bad production day, not only in a design review.

The BFF should not become a second domain layer where business rules are copied from the core services. It should not be the only place authorization exists. It should not become a generic API for every future client. Once that happens, the team has recreated the shared backend problem with a newer name.

BFF should own BFF should avoid owning
Payload shaping, orchestration, UI-specific fallback, client telemetry, and frontend release alignment. Core business rules, durable domain state, enterprise workflows, and public multi-client contracts.
Authorization checks needed to protect the specific experience and hide fields the client must not see. The only copy of permission logic that domain services must also enforce.

BFF is useful because it protects two things at the same time: the frontend experience and the domain services underneath it. The frontend gets a backend that speaks its language. The core services avoid becoming screen-shaped compromise APIs.

What I learnt is that a good BFF makes frontend work faster without turning the domain layer into a screen rendering assistant.

#BFF #Architecture #GraphQL #REST #APIDesign #SoftwareEngineering