The Hidden Cost of BFFs: Why We're Moving Beyond the Backend-For-Frontend Pattern

At my company—a mid-sized SaaS organization with a 100-person R&D team—we’re in the midst of a structural simplification. After years of operating with a 3-tier architecture (Frontend Applications → BFFs → Domain Services), we're shifting toward what I’ve come to call a 2.5-tier architecture: Frontend Applications, a (GraphQL) Router, and Domain Services. This architecture extends upon the traditional api-gateway pattern to solve product growing pains.

This transition is not driven by dogma or fashion, but by hard-earned lessons about internal quality, architectural clarity, and efficiency—especially in the age of AI-assisted engineering.

Why BFFs Made Sense (Until They Didn’t)

When we adopted the Backend-for-Frontend (BFF) pattern, it was the right decision. We had multiple distinct applications with separate user bases, unique use cases, and independent teams. Standardizing on a shared tech stack (i.e. Node.js, Express, TypeScript, Apollo GraphQL, Sequelize) allowed us to invest in tools and conventions while enabling product velocity.

The pattern worked—until it didn’t.

Over time, the boundaries between applications blurred. For example, stateful backend logic for a chat feature originally scoped for an independent mobile app appeared in the admin dashboard. Other domains previously unshared were getting reused across apps. Suddenly, code was duplicated across BFFs, and inconsistencies crept in. A deeper problem surfaced: the erosion of clarity between application business logic and domain business logic.

In the abstract, our 3-tier model looked clean:

  1. Frontend Apps (web/mobile) – UI and presentation logic

  2. BFFs – Access control, aggregation, caching, and “application business logic”

  3. Domain Services – Domain logic (semantics aside, what is architected to be reusable and “core” to the applications and business).

But that middle layer, the BFF, became an architectural junk drawer—an unclear boundary where logic accumulated simply because it was the easiest place to put it.

Vertical Ownership and the BFF Trap

Our organization emphasizes vertical ownership: small, fullstack teams responsible for delivering end-to-end features. It's a great model for product velocity, but it subtly encourages anti-patterns when the architecture doesn't align.

Since BFFs were co-owned by frontend engineers and colocated in their repos, logic that belonged in Domain Services—such as side effects, state transitions, or permissions (ABAC in our case)—was often implemented in the BFF. It worked until that same logic needed to be reused by another app. Then it broke down. But by that point, the coupling had already calcified.

The deeper issue? Many engineers didn’t recognize this as an architectural problem. They just saw friction, not the boundary violation that caused it.

The Pain of Local Development

Add to this another cost: local development complexity.

To run an end-to-end feature, fullstack engineers needed to spin up three services: the frontend app, its BFF, and at least one domain service. Tooling and a separate shift to monorepos helped, but the burden remained—and was deeply felt by new engineers and those working across teams.

A fullstack developer experience should be productive, not punitive.

Toward Simplicity: The 2.5-Tier Architecture

Our new direction embraces simplification.

Instead of frontend teams writing logic in BFFs, we’re pushing application and domain logic into the right places: the frontend or the domain service. BFFs are being deprecated in favor of a shared federated GraphQL router. This router—integrated into our monorepo tooling—is fast to spin up locally and acts as the single interface between clients and domain services.

We’re reviewing each BFF route manually. Where possible, we generalize and relocate logic into the relevant domain service. We're also moving access control into clearly defined RBAC/ABAC policies at the service level—centralizing where security lives and improving its testability. This will be an intermediate step to a proper policy engine as use cases are revisited and properly captured.

During such a migration, it has proven a necessity to revisit the context of a domain. As with any system that becomes “legacy” over time, logic abstracted incorrectly or disguised as something else in intent (i.e. as caching) may be better thought of as a feature in the domain service. This was visible in various compliance features we had built over time. Sequential network calls were discovered which can now be modelled as part of the access patterns in the revisited domain.

Unexpected Wins: AI, Quality, and Focus

One unexpected but welcome benefit of this shift? Better AI-generated pull requests.

When architectural boundaries are clear, tools like Devin.AI become genuinely useful. They can handle routine or boilerplate work—hooking up endpoints, validating inputs, or writing glue code—without wandering into ambiguous territory. That frees up engineers to focus on more complex, high-leverage tasks. And it keeps our sprints cleaner, with fewer distractions from tangled mid-layer logic.

Final Thoughts

Backend-for-Frontend is a thoughtful pattern with real benefits—but it’s not forever. When boundaries blur and logic spreads, BFFs can quietly undermine internal quality and developer efficiency.

If your architecture is starting to creak under the weight of duplicated logic, unclear responsibilities, or complex local dev environments, it might be time to re-evaluate. For us, moving beyond BFFs is a bet on clarity, reuse, and scale—and so far, it’s one that’s paying off.

Previous
Previous

Optimizing Engineering Documents for the Reader