Provider Organization
Organize providers by feature and responsibility to create a scalable, maintainable Riverpod application.
What is it?
Provider organization is the practice of arranging providers so they are easy to find, understand, and maintain.
As a project grows, the number of providers can quickly increase. Keeping them organized prevents duplication, reduces coupling, and makes dependencies easier to manage.
A good organization strategy ensures every provider has a clear owner and purpose.
Why does it exist?
Without a clear organization strategy, projects often end up with:
- Hundreds of providers in one file
- Duplicate providers for the same data
- Circular dependencies
- Providers that are difficult to locate
- Unclear ownership of business logic
Proper organization keeps the codebase predictable and easier to extend.
Syntax
Keep providers inside their feature
features/
└── products/
├── data/
├── presentation/
├── providers/
│ ├── product_provider.dart
│ ├── product_list_provider.dart
│ └── cart_provider.dart
└── widgets/
Explanation:
- Providers live alongside the feature they belong to.
- Related code remains together.
Separate shared providers
core/
└── providers/
├── api_provider.dart
├── dio_provider.dart
└── shared_preferences_provider.dart
Explanation:
- Only application-wide dependencies belong in
core. - Feature-specific providers should stay within their respective features.
Create derived providers
final fullNameProvider = Provider<String>((ref) {
final user = ref.watch(userProvider);
return '${user.firstName} ${user.lastName}';
});
Explanation:
- Derived providers compute values from existing providers.
- Avoid duplicating state across multiple providers.
Mental Model
Poor organization:
providers/
├── auth.dart
├── user.dart
├── cart.dart
├── orders.dart
├── settings.dart
├── dashboard.dart
├── notifications.dart
└── ...
Feature-based organization:
features/
├── auth/
│ └── providers/
├── cart/
│ └── providers/
├── orders/
│ └── providers/
└── settings/
└── providers/
Each feature owns its providers, making responsibilities clear and reducing coupling.
Examples
Simple Example
features/
└── profile/
├── profile_provider.dart
├── profile_repository.dart
└── profile_page.dart
Explanation:
- All profile-related code is grouped together.
Real-World Example
lib/
├── core/
│ ├── providers/
│ ├── network/
│ └── services/
│
├── features/
│ ├── authentication/
│ │ └── providers/
│ │
│ ├── products/
│ │ └── providers/
│ │
│ ├── orders/
│ │ └── providers/
│ │
│ └── settings/
│ └── providers/
│
└── shared/
└── widgets/
Explanation:
- Global infrastructure is centralized.
- Business logic remains inside each feature.
- Features can evolve independently.
When to Use
Organize providers carefully when:
- Building medium or large applications.
- Working with multiple developers.
- Creating reusable features.
- Splitting the application into modules.
- Maintaining long-lived projects.
When NOT to Use
A small prototype with only a few providers may not need a complex folder structure.
Start simple, then reorganize as the application grows.
Avoid introducing unnecessary abstraction for small projects.
Best Practices
- Organize providers by feature.
- Keep one responsibility per provider.
- Use derived providers instead of duplicating state.
- Place global dependencies in
core. - Keep provider files focused and small.
- Use consistent naming across all providers.
- Document provider dependencies when they become complex.
Common Mistakes
Putting every provider in one file
Wrong:
providers.dart
Explanation:
- The file becomes difficult to navigate.
- Merge conflicts become more frequent in team environments.
Correct:
features/
├── auth/providers/
├── cart/providers/
└── profile/providers/
Explanation:
- Providers remain close to the features they support.
- Files stay manageable.
Duplicating state
Wrong:
final userProvider = Provider<User>(...);
final profileProvider = Provider<User>(...);
Explanation:
- Two providers own the same data.
- Keeping them synchronized becomes difficult.
Correct:
final userProvider = Provider<User>(...);
final profileProvider = Provider<Profile>((ref) {
final user = ref.watch(userProvider);
return user.profile;
});
Explanation:
- One provider owns the source of truth.
- Other providers derive values from it.
Mixing global and feature-specific providers
Wrong:
core/
└── providers/
├── auth_provider.dart
├── cart_provider.dart
├── order_provider.dart
└── profile_provider.dart
Explanation:
- Feature logic leaks into the application's shared layer.
Correct:
core/
└── providers/
├── dio_provider.dart
└── logger_provider.dart
features/
├── auth/providers/
├── cart/providers/
└── orders/providers/
Explanation:
- Shared dependencies stay global.
- Business logic stays within its owning feature.
Related APIs
- Provider
- NotifierProvider
- AsyncNotifierProvider
- Provider Families
ref.watch()ProviderScope
Summary
Well-organized providers make Riverpod applications easier to navigate, maintain, and scale. Group providers by feature, keep each provider focused on a single responsibility, use derived providers instead of duplicating state, and reserve global providers for shared infrastructure.