skills/flutter-architecture/references/feature-first.md

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
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.dartclass TodoRepository
  • todo_list_view.dartclass TodoListView
  • fetch_todos.dartclass 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:

  1. Move common logic to shared/
  2. Use dependency injection with a stable interface
  3. 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

  1. Create feature folder:

    features/new_feature/
    ├── data/
    └── presentation/
    
  2. Add subfolders based on complexity:

    • Simple: Skip domain/, use only data/ and presentation/
    • Complex: Add domain/ for use-cases
  3. Implement bottom-up:

    • Add models in data/ or domain/models/
    • Add services in data/services/
    • Add repositories in data/repositories/
    • Add viewmodels in presentation/viewmodels/
    • Add views in presentation/views/
  4. 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';
    
  5. 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/ or presentation/
  • 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:

  1. Start with new features:

    • Add new features using feature-first
    • Keep old code in layer-first
  2. 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
    
  3. Move shared code to shared/:

    • Identify cross-feature code
    • Move to appropriate shared/ folder
    • Update imports
  4. 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