10 KiB
Feature-First Architecture
Organize code by features instead of layers for better maintainability and team scalability.
Overview
Feature-first organizes code by business features, with each feature containing all layers needed for that feature.
lib/
├── features/
│ ├── auth/
│ │ ├── data/
│ │ │ ├── repositories/
│ │ │ └── services/
│ │ ├── domain/
│ │ │ ├── models/
│ │ │ └── use-cases/
│ │ └── presentation/
│ │ ├── views/
│ │ └── viewmodels/
│ ├── todos/
│ │ ├── data/
│ │ ├── domain/
│ │ └── presentation/
│ └── settings/
│ ├── data/
│ ├── domain/
│ └── presentation/
├── shared/
│ ├── core/
│ │ ├── constants/
│ │ ├── theme/
│ │ └── utils/
│ ├── data/
│ │ ├── models/
│ │ └── services/
│ └── ui/
│ ├── widgets/
│ └── components/
└── main.dart
Feature-First vs Layer-First
Layer-First (Traditional)
lib/
├── data/
│ ├── repositories/
│ ├── services/
│ └── models/
├── domain/
│ ├── use-cases/
│ └── entities/
├── presentation/
│ ├── views/
│ └── viewmodels/
└── shared/
When to use:
- Small to medium apps
- Few features (<10)
- Solo developers or small teams
- Simple business logic
Pros:
- Clear separation by layer
- Easy to find components by type
- Less nesting
Cons:
- Harder to delete features
- Changes often span multiple folders
- Tangled dependencies between layers
Feature-First (Recommended for teams)
lib/
├── features/
│ ├── auth/
│ │ ├── data/
│ │ ├── domain/
│ │ └── presentation/
│ └── todos/
│ ├── data/
│ ├── domain/
│ └── presentation/
└── shared/
When to use:
- Medium to large apps
- Many features (10+)
- Team development (2+ developers)
- Complex business logic
- Frequently adding/removing features
Pros:
- Features are self-contained units
- Easy to add/remove entire features
- Clear feature boundaries
- Reduced merge conflicts
- Teams work independently on features
Cons:
- More nesting in folder structure
- Shared code requires careful organization
- Can duplicate similar code across features
Feature Structure
Each feature contains all layers for that feature:
features/
└── todos/
├── data/
│ ├── repositories/
│ │ └── todo_repository.dart
│ └── services/
│ └── todo_api_service.dart
├── domain/
│ ├── models/
│ │ └── todo.dart
│ └── use-cases/
│ └── fetch_todos.dart
│ └── create_todo.dart
│ └── delete_todo.dart
└── presentation/
├── views/
│ └── todo_list_view.dart
└── viewmodels/
└── todo_list_viewmodel.dart
Naming Conventions
Feature folders: Plural, lowercase, underscore-separated
auth/,todos/,user_profile/
Component folders: Plural, lowercase
data/,domain/,presentation/repositories/,services/,models/views/,viewmodels/
Files: snake_case for regular files, camelCase for classes
todo_repository.dart→class TodoRepositorytodo_list_view.dart→class TodoListViewfetch_todos.dart→class FetchTodos
Shared Code
Code used across multiple features goes in shared/:
shared/
├── core/
│ ├── constants/
│ │ └── app_constants.dart
│ ├── theme/
│ │ ├── app_colors.dart
│ │ └── app_text_styles.dart
│ └── utils/
│ ├── extensions.dart
│ ├── validators.dart
│ └── formatters.dart
├── data/
│ ├── models/
│ │ └── common_models.dart
│ └── services/
│ ├── network_service.dart
│ ├── local_storage_service.dart
│ └── analytics_service.dart
└── ui/
├── widgets/
│ ├── app_scaffold.dart
│ ├── loading_indicator.dart
│ └── error_widget.dart
└── components/
├── app_button.dart
├── app_text_field.dart
└── app_card.dart
What goes in shared/core/
- Constants: App-wide constants (API URLs, timeouts)
- Theme: Colors, text styles, themes
- Utils: Extensions, validators, formatters used across features
What goes in shared/data/
- Models: Domain models used by multiple features (User, etc.)
- Services: Platform services (network, storage, analytics)
What goes in shared/ui/
- Widgets: Reusable UI components (scaffolds, indicators)
- Components: Reusable UI elements (buttons, cards, inputs)
Dependency Rules
Within a Feature
presentation/ -> domain/ -> data/
presentation/ -> data/ // simple flows can skip domain
presentation/ -> shared/ui/
data/ -> shared/data/
- Presentation ViewModels can use Domain, Data for simple flows, and Shared/UI. Views should still talk to ViewModels, not Services.
- Domain layer can use feature data abstractions and shared pure utilities.
- Data layer can use Shared/Data services or models.
- Domain layer is optional. Add use-cases only for complex, reused, or multi-repository business logic.
Between Features
Features should NOT import each other's implementation files.
If Feature A needs Feature B's functionality:
- Move common logic to
shared/ - Use dependency injection with a stable interface
- Consider if features should be merged
// ❌ BAD: Feature depending on another feature
import 'package:my_app/features/auth/data/repositories/auth_repository.dart';
// ✅ GOOD: Using shared service
import 'package:my_app/shared/data/services/auth_service.dart';
// ✅ GOOD: Depending on a stable shared interface provided through DI
import 'package:my_app/shared/data/auth_session.dart';
When to Add a New Feature
-
Create feature folder:
features/new_feature/ ├── data/ └── presentation/ -
Add subfolders based on complexity:
- Simple: Skip
domain/, use onlydata/andpresentation/ - Complex: Add
domain/for use-cases
- Simple: Skip
-
Implement bottom-up:
- Add models in
data/ordomain/models/ - Add services in
data/services/ - Add repositories in
data/repositories/ - Add viewmodels in
presentation/viewmodels/ - Add views in
presentation/views/
- Add models in
-
Add exports:
// features/new_feature/new_feature.dart export 'data/repositories/new_feature_repository.dart'; export 'presentation/views/new_feature_view.dart'; export 'presentation/viewmodels/new_feature_viewmodel.dart'; -
Register in DI container (main.dart)
Best Practices
1. Barrel Files
Export feature contents from barrel file:
// features/todos/todos.dart
library todos;
export 'data/repositories/todo_repository.dart';
export 'domain/models/todo.dart';
export 'domain/use-cases/fetch_todos.dart';
export 'presentation/views/todo_list_view.dart';
export 'presentation/viewmodels/todo_list_viewmodel.dart';
Usage:
import 'package:my_app/features/todos/todos.dart';
// Now have access to Todo, TodoRepository, etc.
2. Lazy Loading
For large apps, use lazy loading:
// In router
GoRoute(
path: '/todos',
builder: (context, state) => const LazyLoad(
builder: () => const TodosView(),
),
);
3. Feature Scoping
Keep feature dependencies minimal:
- Use
shared/for common code - Avoid imports from another feature's
data/orpresentation/ - When needed, use dependency inversion through an interface
4. Testing
Each feature is testable independently:
features/
└── todos/
├── data/
│ └── repositories/
│ └── todo_repository_test.dart
├── domain/
│ └── use-cases/
│ └── fetch_todos_test.dart
└── presentation/
└── viewmodels/
└── todo_list_viewmodel_test.dart
Migration from Layer-First
Gradual migration strategy:
-
Start with new features:
- Add new features using feature-first
- Keep old code in layer-first
-
Migrate one feature at a time:
// Before (layer-first) data/ ├── todo_repository.dart └── user_repository.dart presentation/ ├── todo_view.dart └── user_view.dart // After (feature-first) features/ ├── todos/ │ ├── data/repositories/todo_repository.dart │ └── presentation/views/todo_view.dart └── users/ ├── data/repositories/user_repository.dart └── presentation/views/user_view.dart -
Move shared code to
shared/:- Identify cross-feature code
- Move to appropriate
shared/folder - Update imports
-
Update barrel files for clean imports
Example: Complete Feature
features/
└── todos/
├── todos.dart // Barrel file
├── data/
│ ├── models/
│ │ └── todo_model.dart // DTOs from API
│ ├── repositories/
│ │ └── todo_repository.dart // SSOT for todos
│ └── services/
│ └── todo_api_service.dart // API calls
├── domain/
│ ├── models/
│ │ └── todo.dart // Domain entity
│ └── use-cases/
│ ├── fetch_todos.dart
│ ├── create_todo.dart
│ └── update_todo.dart
│ └── delete_todo.dart
└── presentation/
├── viewmodels/
│ └── todo_list_viewmodel.dart
└── views/
└── todo_list_view.dart
This structure provides:
- Complete feature encapsulation
- Clear layer separation within feature
- Easy to add/remove entire feature
- Minimal dependencies on other features