Why we made the frontend dumb
A real-world decision about ownership, leverage, and why sometimes the best thing a frontend can do is step out of the way.
For a long time, I let complex user flows creep into the frontend because it felt like the easiest place to coordinate them.
It sounds practical. The UI is closest to the user. It’s where all the pieces come together.
In reality, that instinct caused more problems than it solved.
This is the story of a moment when we made the frontend dumb and why it worked.
The context
We were building an onboarding flow meant to be reused across multiple features in an enterprise product.
Not a one-off wizard, but a shared entry point that existing and future features would plug into over time.
There was a deadline, but the flow itself wasn’t mission-critical.
What mattered was visibility. Upper management wanted to see it live.
That combination is risky:
- evolving requirements
- shared ownership
- pressure to show progress rather than correctness
At that stage, we were letting things form before locking in ownership.
The frontend and backend teams were both experienced.
They just walked in with very different instincts.
The initial mismatch
Before we agreed on who owns what, something became obvious:
- Frontend assumed it would drive the flow
steps, branching, conditions, edge cases. - Backend assumed it would mostly return data and validations.
The result was predictable:
- the frontend kept pulling more and more flow logic into the UI
- the backend stayed relatively thin
- every change turned into a coordination problem
No one was careless.
But the system was becoming expensive to change before it even shipped.
The hidden frontend cost
Another problem surfaced as the flow grew.
Some onboarding paths touched multiple large features in a single run.
From the frontend side, that meant:
- pulling state from several big parts of the app that were never meant to be wired together
- juggling data coming from places that normally don’t even know about each other
- stitching together concerns that usually live far apart
Onboarding slowly turned into a dumping ground for unrelated frontend concerns.
Every new step made the UI harder to reason about.
Not because of bad code, but because the flow itself spanned too much surface area.
The frontend was becoming fragile in exactly the places that were supposed to be easy to change.
The wrong assumption
The assumption we had to unlearn was simple:
Onboarding is a UX problem, so the frontend should own it.
That belief holds in many cases.
It didn’t hold here.
This onboarding flow was:
- shared across features
- expected to change frequently
- tightly coupled to backend business rules
- touching multiple large frontend areas at once
Putting the flow logic in the frontend meant duplicating knowledge and making unrelated features depend on each other.
The decision
We made a clear call:
The backend would define the entire onboarding flow.
The frontend would act as a strict renderer.
In practice:
- the backend returned a full description of what to show next
- the frontend did only minimal, local validation
- no business decisions lived in the UI layer
From the frontend’s point of view, actions became declarative.
A button like “Mark as complete” expressed intent, not behavior.
Whether that action triggered one update or a chain of backend operations wasn’t a frontend concern anymore.
If the backend returned a new step for a feature, the frontend could already render it without special handling.
A side effect of this shift:
- the frontend no longer had to glue together state from several big features
- onboarding stopped being the place where unrelated parts of the UI kept tripping over each other
This wasn’t about making the frontend “simpler” by pushing work elsewhere.
It forced the backend to take full responsibility for the flow, including cases it previously left vague.
What we intentionally gave up
We knowingly lost:
- local control in the frontend
- the ability to quickly patch logic in UI code
- the comforting feeling that “we can fix this without touching the backend”
We paid for this in lost flexibility on the frontend and in how quickly UI engineers could patch behavior locally.
What we gained instead
The upside showed up fast:
- onboarding changes no longer required frontend redeploys
- existing features adopted the flow with minimal changes
- new features plugged in without rewriting logic
- fewer sync bugs between frontend and backend
- noticeably less coupling across large frontend areas
The real win wasn’t elegance.
It was a much lower cost of change.
More importantly, there was finally a single source of truth.
Conversations stopped being about where logic should live
and started being about what the flow should actually do.
In that context, “dumb” didn’t mean passive.
It meant declarative: the frontend expressed intent, the backend owned behavior.
The human cost
This wasn’t just a technical refactor.
Convincing the backend side took time.
Multiple meetings. People in different time zones. Repeated explanations.
Agreeing on a response shape that worked for all features took iteration and compromise.
That friction wasn’t a sign the idea was wrong.
It was the price of changing ownership in a distributed team.
The result
The approach didn’t stay isolated.
It spread to existing flows.
New features adopted it by default.
The system scaled not because it was clever,
but because it was predictable.
The lesson
This wasn’t a frontend pattern.
It was an ownership decision.
Not every frontend problem is solved by adding more frontend logic.
Sometimes, the most effective thing a frontend engineer can do
is to give up control and let the system decide.
Making the frontend “dumb” didn’t make frontend work less senior.
It just moved the leverage to where it actually belonged.