The BLoC Structure That Finally Let Our Flutter App Scale


Let's be honest. Every growing Flutter project eventually hits a wall.
It starts out fun. You're building features, everything works, and the code is clean. Then, a few months pass. More developers join the team. Deadlines get tighter. Suddenly, you're looking at a feature and have no idea where the state logic lives. Changing one small thing breaks three others. Onboarding a new developer takes weeks instead of days.
If you've ever felt that pain, you're not alone. We've all been there.
It taught us a valuable lesson: the secret to scaling isn't just picking a state management library like BLoC. It's about being ruthlessly disciplined in how you structure it.
This isn't some academic "clean architecture" theory. This is the real-world, battle-tested playbook we now live by.
My Guiding Philosophy: A "Boring" Codebase is a Good Codebase
It took me years to appreciate this, but my goal as a senior dev is to make our codebase as "boring" and predictable as possible.
"Boring" means I can jump into any feature, even one I didn't write, and I instantly know where to find the UI, the state logic, and the data models. It means a junior dev can be productive on their first day because the patterns are consistent everywhere.
Here's how we achieve that boring, beautiful consistency.
Rule #1: We ONLY Use a Feature-First Folder Structure
Everything is feature-driven.
When we start a new feature, say, "product reviews," we create a single folder for it. Everything related to product reviews lives inside.
lib/src/features/authentication/
├── data/
│ ├── models/
│ └── repositories/
├── presentation/
│ ├── bloc/
│ ├── view/
│ └── widgets/
The beauty of this is that if we need to change the authentication feature, we live inside this one folder. If a developer is working on it, they aren't causing merge conflicts with someone working on the "user profile" feature. It's self-contained, clean, and just makes sense.
Rule #2: We Slayed Our Boilerplate with a Generic State
You know the drill. For every API call, you need states for Loading
, Success
, and Failure
. In the beginning, we were writing these same three classes inside every single BLoC's state file. It was soul-crushing, repetitive work.
This was probably the biggest productivity game-changer we implemented. We created a single, generic DataState
class that we now use across the entire application.
It looks something like this. Feel free to steal it.
// lib/src/core/utils/data_state.dart
// (Your reusable DataState code from the previous example goes here)
abstract class DataState<T> { ... }
class DataSuccess<T> extends DataState<T> { ... }
class DataFailure<T> extends DataState<T> { ... }
class DataLoading<T> extends DataState<T> { ... }
Now, our BLoC definitions are clean and simple: class AuthenticationBloc extends Bloc<AuthenticationEvent, DataState<List<AuthenticationModel>>>
We instantly know it will have a loading state, a failure state with an error, and a success state with a list of authentication. No more reinventing the wheel for every single feature.
Rule #3: A BLoC Has Only One Job. Period.
"Should I just add this logic to the UserProfileBloc
?"
I get asked this all the time. My answer is almost always a question back: "Is it part of the user's core profile, or is it a related, but separate, piece of data?"
Our rule is simple: a BLoC is responsible for the state of one logical thing.
A UserProfileBloc
handles the user's name, email, and profile picture. It should absolutely NOT also handle the list of their past orders. That's a separate logical entity. That deserves its own BLoC.
When you let a BLoC do too many things, you create a "God Object"—a monstrous class that's impossible to test and terrifying to modify. By keeping them small and focused, they stay nimble and easy to understand. If a screen needs data from both, that's fine! Provide both Blocs to that part of the widget tree.
It's Your Turn
Look, this isn't the only way to build a Flutter app, but it's the way that has brought scalability to our team.
It’s about creating a system that frees you up to be creative.
I'm genuinely curious—how does your team solve these problems? What architectural rules do you live by? Drop a comment below. Let's talk about it.
Subscribe to my newsletter
Read articles from Dhruva Shaiva directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
