Playbook

How we keep a Flutter codebase maintainable as it grows

Most apps don’t die from hard problems — they die from a thousand small shortcuts that make every change slower than the last. How we structure Flutter projects, draw the line between UI and logic, and keep velocity from decaying as the app grows.

ARAbishek Reddy
Abishek Reddy, Founder
Jun 20266 min read

The hardest part of building an app isn’t the first version. It’s the fortieth change to the first version. Almost every codebase starts clean and quick to work in, and almost every one slows down over time — not because the problems get harder, but because the structure quietly rots until every new feature means touching ten files and hoping. By the time it’s obvious, it’s expensive to undo.

We build mostly in Flutter, and most of our work on architecture is really work on keeping that decay rate low. None of it is clever. It’s a handful of boring rules applied consistently — which is the only kind of architecture that survives contact with a deadline.

Organise by feature, not by type

The default way to lay out a project is by what things are: all the screens in one folder, all the models in another, all the controllers in a third. It looks tidy on day one and turns miserable by month three, because a single feature is now smeared across five folders and changing it becomes a scavenger hunt.

We organise by feature instead: everything for “chat” lives in a chat folder — its screens, its state, its models, its API calls. A new engineer can understand one feature, or delete it, without spelunking the whole tree. The folders map to how people actually think about the product, which is the entire point.

Keep UI and logic on opposite sides of a line

The single most valuable boundary in an app is the one between what the screen looks like and what the app does. We use the BLoC pattern in Flutter to enforce it: widgets render state and emit events, and all the real decisions — what to fetch, what to validate, what happens next — live in a separate layer the UI can’t reach around.

The payoff is that you can change the look without fear of breaking behaviour, and test the behaviour without rendering a single pixel. When the two are tangled — business rules sitting inside button handlers — every visual tweak risks a logic bug, and nothing can be tested without driving the UI. That tangle is most of what people mean when they say “legacy code.”

You don’t feel good architecture the day you write it. You feel its absence two years later, in how long every small change takes.

Make the data layer a wall, not a window

Screens should never know where their data comes from. Whether a profile is fresh from the API, cached on disk, or stubbed in a test should be invisible to the widget that displays it. We put a repository layer in between: the app asks the repository for a profile, and the repository decides how to actually get it.

This sounds academic until the day you switch backends, add offline caching, or need to write a test without a live server — at which point a clean data boundary turns a rewrite into a one-file change. It’s cheap insurance bought early.

Name your shortcuts out loud

Shortcuts aren’t the enemy — pretending they’re free is. Sometimes the right call under a deadline is to hard-code the thing, skip the abstraction, copy-paste the widget. What quietly kills a codebase is doing that silently, so the shortcut becomes permanent by default.

So we name them: a short comment, a tracked task, a line in the pull request that says “this is temporary, and here’s why.” It costs almost nothing, and it’s the difference between technical debt you chose and technical debt you discover.

Why we bother

Clients sometimes ask why we “spend” time on structure instead of features. The honest answer is that we’re not trading features for structure — we’re trading this month’s velocity for every month after it. A clean codebase isn’t about elegance; it’s about keeping the app cheap to change, because the one certainty with a successful product is that it will need to change far more than you planned.

The test of architecture is never how it looks the day it ships. It’s whether the team is still moving fast in year two — and that’s decided by the unglamorous calls made in week one.

Building something like this?

That's the work we do every day. Tell us what you're shipping.

Start a project