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