How I Organize Large Flutter Apps
FlutterPulseThis article was translated especially for the FlutterPulse channel. In this channel you will find many interesting things related to Flutter. Don't forget to subscribe! 🚀

How I Organize Large Flutter Apps
Let's make it happen. "Group by feature, not by type." You've probably read that sentence a dozen times. It sounds good in a tweet. It's even better when you've spent an hour hunting for the three files that implement a single screen.
Quick summary (if you're skimming)
- Feature-based = grouping code by vertical slices (features) rather than types (widgets, models, services).
- Pros: easier navigation, encapsulation, independent development, simpler testing, better boundaries.
- Cons: possible duplication, initial learning curve, unclear shared-vs-core split.
- Do this: keep a small
core/orshared/, use thin public interfaces for features, prefer composition over deep inheritance, keep feature boundaries enforced via code review and CI. - Patterns to copy: feature module (with sub-layers), package-per-feature (for very large apps), route-driven lazy loading, feature adapters.
Why feature-based architecture helps (real benefits)
- Faster mental model: When you want to change "cart checkout", open
features/cart/— everything's there: UI, models, VMs, tests. No cognitive juggling. - Encapsulation & clear boundaries: Features can define a small public API and hide private helpers — reduces accidental coupling.
- Parallel development: Teams can work on different features with low merge-conflict surface (files are separate).
- Better testing: Feature-level integration tests are easier to write when the feature is a coherent unit.
- Easier extraction or reuse: Well-organized features are easier to extract into packages later (if needed).
Common misconceptions (so you don't repeat them)
- "Feature-based = no shared code." False. You WILL have shared utilities. The point is to minimize cross-feature coupling, not eliminate sharing.
- "Everything goes in features, end of story." Also false. Core services (auth, network, theme) belong in a clearly named
core/orshared/package. - "It removes the need for architecture." It doesn't — it makes architecture explicit. You still need DI, interfaces, and tests.
Typical folder structures (copyable)
Small-to-medium app (single repo):
lib/
core/ // app-wide services, theme, utils
shared/ // shared widgets, components
features/
auth/
data/
domain/
ui/
auth_feature.dart
cart/
data/
domain/
ui/
cart_feature.dart
app.dartMedium-to-large app (feature modules with public API):
lib/
core/
features/
product/
lib/
product.dart // public exports
src/
ui/
data/
checkout/
lib/
checkout.dart
src/
main.dartConcrete patterns & recommended rules
1) Feature public API
Each feature exposes a tiny entry point (e.g., cart_feature.dart) that exports only what other modules need (routes, factories, a provider key).
library cart_feature;
export 'src/ui/cart_screen.dart';
export 'src/cart_module.dart'; // provides DI bindings or route2) Keep core/ minimal and explicit
core/ should contain services that by definition must be global: AuthService, HttpClient, AppTheme, maybe AppRouter.
3) Dependency Injection at feature boundary
class CartModule {
static Widget build() {
return Provider<CartService>(
create: (_) => CartServiceImpl(api: getIt<Api>()),
child: CartScreen(),
);
}
}4) Ownership & team boundaries
Document which team/person owns each feature. Ownership reduces accidental cross-feature edits and drives better APIs.
5) Routing: central registration vs feature registration
- Central router registers feature routes (simple apps)
- Feature self-registers via a route provider (scales better)
6) State management per feature
Prefer local state inside feature boundaries (Bloc/Cubit, Provider, Riverpod provider). Avoid a single global store with thousands of keys — it defeats encapsulation.
7) Avoid circular dependencies
Enforce this with code review and CI (tools like dep_check or custom scripts). Circular imports are the fastest route to accidental coupling.
Testing strategy
- Unit tests for the domain logic inside
features/<x>/domain/. - Widget tests for
features/<x>/ui/. - Feature/integration tests that spin up one or more features with mocked collaborators.
- Contract tests for the feature public API so upgrades or refactors don't break consumers.
When feature-based architecture is not the best choice
- Tiny apps (one or two screens): overhead may not be worth it
- Ultra-highly shared domain (lots of cross-cutting concerns with no clean partitioning)
- If your team lacks discipline, feature folders without enforced boundaries become a mess
Migration plan (how to switch an existing app safely)
- Create
features/folder and pick a small feature to migrate (e.g., Settings). - Move files, preserve imports, and add
feature.dartpublic entry file. - Add DI wrapper for the feature (module/provider).
- Run tests & CI — fix import paths.
- Repeat feature-by-feature, not all at once.
- Refactor shared utilities: if utilities are only used by one feature, move them in. If used by many, keep in
shared/.
Pitfalls & how to avoid them
- Pitfall:
shared/becomes a dumping ground. Fix: enforce a "one caller > move to feature" rule. - Pitfall: duplicated code across features. Fix: extract reusable logic into small focused packages or utilities, not giant swiss-army helpers.
- Pitfall: feature boundaries are only folder names (no enforcement). Fix: use code owners, PR checks, and dependency tools to prevent imports across feature internals.
- Pitfall: too many tiny features (over-fragmentation). Fix: group very small related features into a single module.
Final thoughts — practical mindset
Feature-based architecture is not a silver bullet. It's a practice that rewards discipline: small public APIs, clear ownership, and deliberate sharing. When used well it dramatically reduces cognitive load and speeds onboarding. When used poorly it's just folder noise.
If you take away one action after reading this: pick one non-trivial feature and modularize it this sprint — expose a clear public API, keep internals private, and add a small integration test. The benefits become visible fast.
Happy coding! 🚀