System Design: The BFF Pattern
Why I built a Backend-for-Frontend at a marketplace serving millions, how it cut cross-team dependencies by 40%, and when you should refuse to build one.
2 min read
At a large e-commerce marketplace serving millions of users, feature delivery kept stalling on the same conversation: the frontend team needed a slightly different shape of data than the platform APIs returned, and the platform team (rightly) didn't want to bend a shared API around one screen. Multiply that by every team consuming those APIs, and you get an organization-shaped bottleneck.
The fix wasn't a faster API. It was an ownership boundary: a Backend-for-Frontend.
The shape of the problem
The BFF sits between one client and many platform services. It aggregates, reshapes, and, critically, is owned by the team that owns the screen. When the item page needs a new composite view, the frontend team ships it in the BFF without negotiating a platform API change. That single ownership shift is what reduced cross-team dependencies by around 40%: most changes stopped requiring two teams.
What belongs in a BFF
- Aggregation. One round trip for the client instead of five.
- Reshaping. Platform models in, view models out.
- Client-specific policy. Caching per screen, response trimming for mobile.
And what doesn't: business rules. The moment inventory logic lives in the BFF, you have two sources of truth and the platform team can no longer reason about invariants. In review, my test was one question: "If we deleted the BFF tomorrow, would any business rule vanish with it?" The answer had to be no.
The failure mode: N BFFs, one team
BFFs go wrong when they're created for architecture-diagram reasons rather than ownership reasons. If the same team owns the client, the BFF, and the platform API, the BFF is just a hop with extra latency and a second deploy. I've refused to build them in that situation. A well-designed platform endpoint was the honest answer.
// The BFF's job: fan out, tolerate partial failure, return a view model.
public ItemPageView getItemPage(String itemId) {
var item = itemClient.get(itemId); // required, fail fast
var pricing = supply(() -> pricingClient.get(itemId));
var reviews = supply(() -> reviewClient.top(itemId, 5))
.orElse(ReviewSummary.empty()); // optional, degrade
return ItemPageView.of(item, pricing.join(), reviews.join());
}The orElse(empty()) line carries the design philosophy. An item page with missing reviews is a page. An item page that 500s because the review service hiccuped is an outage. Deciding which dependencies are load-bearing is the actual system design work; the diagram is just the record of it.
Next topic
Authentication