Feature-First Architecture
Organizing your Flutter app by features instead of layers for better scalability, maintainability, and team collaboration.
Feature-First Architecture (also known as Feature-Based or Feature-Driven Architecture) organizes your code by features rather than by layers. Each feature contains all the code it needs — from UI to data — making it a self-contained module that can be developed, tested, and maintained independently.
💡 Key Concept
Instead of organizing by technical layers (presentation, business logic, data), you organize by business capabilities . Each feature represents a user-facing functionality like "Login," "Dashboard," or "Shopping Cart."
❌ Layer-First Structure
- Organized by technical concerns
- Scatters feature code across folders
- Harder to find related code
- More conflicts in teams
- Difficult to extract features
✅ Feature-First Structure
- Organized by business capabilities
- Feature code is colocated
- Easier to find related code
- Fewer team conflicts
- Features can be extracted as packages
Each feature typically follows the same internal structure, maintaining clean architecture principles within the feature boundary:
📁 Feature: weather
weather_remote_datasource.dart
weather_local_datasource.dart
models/
weather_dto.dart
repositories/
weather_repository_impl.dart
weather.dart
repositories/
weather_repository.dart
usecases/
get_weather_usecase.dart
weather_screen.dart
widgets/
weather_card.dart
viewmodels/
weather_viewmodel.dart
📁 Feature: auth
auth_remote_datasource.dart
models/
user_dto.dart
repositories/
auth_repository_impl.dart
user.dart
repositories/
auth_repository.dart
usecases/
login_usecase.dart
login_screen.dart
register_screen.dart
viewmodels/
auth_viewmodel.dart
✅ Benefits of This Structure
- Colocation – All code for a feature is in one place
- Clear boundaries – Features are explicitly defined
- Independent development – Teams can work on different features
- Scalability – Add new features without affecting existing ones
- Modularity – Features can become separate packages
Each feature module should be self-contained and expose only what's necessary to other parts of the app:
import 'package:get_it/get_it.dart';
import 'package:http/http.dart' as http;
import 'data/datasources/weather_remote_datasource.dart';
import 'data/datasources/weather_local_datasource.dart';
import 'data/repositories/weather_repository_impl.dart';
import 'domain/repositories/weather_repository.dart';
import 'domain/usecases/get_weather_usecase.dart';
import 'presentation/viewmodels/weather_viewmodel.dart';
class WeatherFeatureModule {
final GetIt getIt;
WeatherFeatureModule(this.getIt);
void register() {
// Data Sources
getIt.registerSingleton<WeatherRemoteDataSource>(
WeatherRemoteDataSource(
client: http.Client(),
baseUrl: 'https://api.openweathermap.org/data/2.5',
apiKey: 'your-api-key',
),
);
getIt.registerSingleton<WeatherLocalDataSource>(WeatherLocalDataSource());
// Repository
getIt.registerLazySingleton<WeatherRepository>(() {
return WeatherRepositoryImpl(
remoteDataSource: getIt<WeatherRemoteDataSource>(),
localDataSource: getIt<WeatherLocalDataSource>(),
);
});
// Use Cases
getIt.registerFactory<GetWeatherUseCase>(() {
return GetWeatherUseCase(getIt<WeatherRepository>());
});
// ViewModels
getIt.registerFactory<WeatherViewModel>(() {
return WeatherViewModel(getIt<GetWeatherUseCase>());
});
}
}
import 'package:get_it/get_it.dart';
import '../features/weather/weather_di.dart';
import '../features/auth/auth_di.dart';
final getIt = GetIt.instance;
void setupServiceLocator() {
// Register core services that all features need
getIt.registerSingleton<HttpClient>(HttpClient());
// Register each feature module
WeatherFeatureModule(getIt).register();
AuthFeatureModule(getIt).register();
// ... more features
}
Features often need to communicate with each other. Here's how to handle cross-feature dependencies while maintaining feature independence:
shared
or
core
folder for models used across features.
Each feature should depend on these shared models.
AnalyticsService
,
LoggerService
, and
AuthService
should be registered in the core DI and injected into features.
// Shared models used across features
class User {
final String id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
}
class Coordinate {
final double latitude;
final double longitude;
Coordinate({required this.latitude, required this.longitude});
}
import '../../../../core/shared/models.dart';
/// Auth repository interface - depends on shared models
abstract class AuthRepository {
Future<User> login(String email, String password);
Future<void> logout();
Future<User?> getCurrentUser();
}
A central AppCoordinator or Router can orchestrate navigation and communication between features:
import 'package:go_router/go_router.dart';
import '../../features/auth/presentation/screens/login_screen.dart';
import '../../features/weather/presentation/screens/weather_screen.dart';
import '../../features/settings/presentation/screens/settings_screen.dart';
class AppRouter {
static final router = GoRouter(
routes: [
GoRoute(path: '/', builder: (context, state) => LoginScreen()),
GoRoute(path: '/weather', builder: (context, state) => WeatherScreen()),
GoRoute(path: '/settings', builder: (context, state) => SettingsScreen()),
],
);
static void navigateToWeather(BuildContext context) {
context.push('/weather');
}
static void navigateToSettings(BuildContext context) {
context.push('/settings');
}
}
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'core/navigation/app_router.dart';
import 'di/service_locator.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Setup all feature dependencies
await setupServiceLocator();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Feature-First App',
theme: ThemeData(useMaterial3: true),
routerConfig: AppRouter.router,
);
}
}
Follow these steps to migrate to Feature-First Architecture:
lib/features/feature_name/
for each feature
core/
or
shared/
Creating direct dependencies between features makes them hard to test and maintain independently.
Features should depend on interfaces (abstractions) that are injected, not concrete implementations.
A feature should have a single, clear responsibility. Don't put unrelated code in a feature.
Each feature should represent a single user-facing capability (e.g., "Weather" not "Data").
Putting all feature code in a single folder without internal structure defeats the purpose.
Each feature should have data, domain, and presentation folders with clear separation of concerns.
Exposing internal implementation details of a feature to other features creates coupling.
Each feature should only expose what's necessary through well-defined public interfaces.
🎯 Key Takeaway
Feature-First Architecture organizes your Flutter app by business capabilities, not technical layers. Each feature is self-contained with its own data, domain, and presentation layers. This approach scales with your team and codebase, makes features testable in isolation, and reduces conflicts in team development. Use a central router for coordination and shared models for cross-feature communication.