Feature-Modular Project Structure
Feature-Modular Project Structure
As Flutter applications grow beyond toy examples, a flat folder structure — where every file sits under lib/ sorted only by type — becomes a maintenance liability. Locating all files related to a single feature requires jumping across models/, screens/, services/, and widgets/ simultaneously. The solution is a feature-modular (also called vertical slice) architecture: each feature owns its files in one cohesive directory, divided internally into data, domain, and presentation layers.
Why Flat Structures Break Down
Consider an app with authentication, a product catalogue, and a shopping cart. A type-based flat layout looks like this:
lib/models/— user.dart, product.dart, cart_item.dart …lib/screens/— login_screen.dart, product_list_screen.dart, cart_screen.dart …lib/services/— auth_service.dart, product_service.dart, cart_service.dart …lib/widgets/— product_card.dart, cart_badge.dart …
Deleting the cart feature now requires hunting through every folder. Adding a team member to work only on authentication is nearly impossible without inadvertently touching unrelated files. A feature-modular layout eliminates both problems.
The Vertical Slice Layout
Each feature becomes a self-contained directory with three sub-layers:
- data/ — repositories, data sources (remote API clients, local DB helpers), and raw model/DTO classes.
- domain/ — entities (pure business objects), repository interfaces (abstract contracts), and use-case classes that encode a single business rule.
- presentation/ — screens, page widgets, feature-scoped state management (Cubit/Notifier/ViewModel), and small UI-only widgets used only within this feature.
Recommended Directory Tree
lib/
├── core/ // Shared infrastructure
│ ├── network/
│ │ └── dio_client.dart
│ ├── error/
│ │ └── failures.dart
│ └── utils/
│ └── validators.dart
│
├── features/
│ ├── auth/
│ │ ├── data/
│ │ │ ├── datasources/
│ │ │ │ └── auth_remote_datasource.dart
│ │ │ ├── models/
│ │ │ │ └── user_model.dart
│ │ │ └── repositories/
│ │ │ └── auth_repository_impl.dart
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ │ └── user.dart
│ │ │ ├── repositories/
│ │ │ │ └── auth_repository.dart // abstract
│ │ │ └── usecases/
│ │ │ └── login_usecase.dart
│ │ └── presentation/
│ │ ├── cubit/
│ │ │ ├── auth_cubit.dart
│ │ │ └── auth_state.dart
│ │ ├── pages/
│ │ │ └── login_page.dart
│ │ └── widgets/
│ │ └── login_form.dart
│ │
│ └── cart/
│ ├── data/ ...
│ ├── domain/ ...
│ └── presentation/ ...
│
└── main.dart
features/ and core/ split is the key boundary. Code inside a feature folder is feature-private; code inside core/ is shared across all features. Never import from one feature into another — route through core/ or use dependency injection instead.The Three Internal Layers Explained
Each internal layer has a strict dependency direction: presentation → domain ← data. The domain layer knows nothing about Flutter or HTTP; it contains only pure Dart.
- domain/entities/ — immutable business objects with no framework coupling (no
fromJson, nocopyWithannotation libraries). - domain/repositories/ — abstract classes that declare what operations exist (e.g.,
Future<User> login(String email, String password)). The domain layer depends on these contracts, not implementations. - domain/usecases/ — one class per business action; calls the repository interface and returns a result. Keeps business logic out of the presentation layer.
- data/models/ — extend or wrap entities with serialisation (
fromJson/toJson). - data/repositories/ — concrete implementations of the domain repository interfaces, wiring data sources together.
- presentation/ — widgets plus a thin state-management layer (Cubit, Riverpod Notifier, etc.) that calls use cases and exposes UI state.
Domain Layer: Entity, Repository Interface, and Use Case
// features/auth/domain/entities/user.dart
class User {
final String id;
final String email;
final String displayName;
const User({
required this.id,
required this.email,
required this.displayName,
});
}
// features/auth/domain/repositories/auth_repository.dart
abstract class AuthRepository {
Future<User> login(String email, String password);
Future<void> logout();
Future<User?> getCurrentUser();
}
// features/auth/domain/usecases/login_usecase.dart
class LoginUseCase {
final AuthRepository _repository;
const LoginUseCase(this._repository);
Future<User> call(String email, String password) {
return _repository.login(email, password);
}
}
The Core / Common Module
Code that must be reused across features lives in lib/core/. Typical residents include:
- network/ — a configured
Dioorhttpclient, interceptors, and base URLs. - error/ — a sealed
Failurehierarchy so every feature reports errors uniformly. - di/ — dependency injection setup (e.g.,
get_itservice locator registrations). - router/ — route names and the
GoRouter/AutoRouteconfiguration. - theme/ —
ThemeData, colour tokens, and text styles. - utils/ — generic validators, formatters, and extension methods.
- widgets/ — truly reusable UI atoms (loading spinner, error snackbar, avatar) used by two or more features.
core/. Keep core/ lean — only things that are genuinely shared belong there, not things that might be shared someday.Barrel Files and Import Hygiene
Each feature can expose a single barrel file (auth.dart) that re-exports only the public surface of the feature — typically the presentation pages and the DI registration function. Internal data and domain classes stay private to the feature, preventing other features from bypassing the architecture.
Barrel File Pattern
// features/auth/auth.dart — public API of the auth feature
export 'presentation/pages/login_page.dart';
export 'presentation/pages/register_page.dart';
export 'di/auth_di.dart'; // registers auth dependencies
// main.dart imports only the barrel:
import 'features/auth/auth.dart';
// Internal classes like AuthRepositoryImpl are NOT exported
// and therefore cannot be accidentally imported by other features.
cart needs a User object from auth, move the User entity to core/ rather than importing from one feature into another. Feature-to-feature imports are an architecture smell that defeats the purpose of modularisation.Summary
A feature-modular structure organises code by business capability rather than technical type. Each feature folder contains its own data, domain, and presentation layers with a strict inward dependency rule. Shared infrastructure moves to core/. The result is a codebase where individual features can be developed, tested, and even extracted into packages independently — dramatically improving scalability and team collaboration as your Flutter application grows.