Use Case Pattern
Overview
Use cases are plain domain classes that orchestrate business flow.
Core rule: providers/widgets manage state, use cases coordinate logic, repositories perform data access.
When to Use
Use this pattern when:
- •moving business logic out of providers/controllers
- •coordinating multiple repositories or services
- •returning domain values after orchestration
- •updating metadata/entities through immutable copy patterns
- •refactoring large methods into testable orchestration units
Do not use this for simple UI-only state changes.
Core Structure
class ActionUseCase {
const ActionUseCase({
required FeatureRepository repository,
required SecondaryDependency secondary,
}) : _repository = repository,
_secondary = secondary;
final FeatureRepository _repository;
final SecondaryDependency _secondary;
Future<ActionOutput> call({required ActionInput input}) async {
if (input.items.isEmpty) {
throw const ManagedDomainException('Input items are required');
}
final prepared = _prepare(input);
final entity = await _repository.execute(prepared);
return ActionOutput.fromEntity(entity);
}
}
Use local DTOs only when needed by the flow (for example batch update structures or metadata updates). Otherwise return the domain value directly.
Rules:
- •use a simple class (no provider inheritance/annotations for the use case itself)
- •inject dependencies through constructor
- •expose one clear entry point, usually
call(...) - •prefer guard clauses and early returns
- •return domain results; do not mutate UI state directly
Orchestration Pattern
- •Validate inputs with guard clauses.
- •Resolve dependencies/IDs needed for execution.
- •Execute delegated operations (repository or other use case).
- •Return the domain value for the next layer.
- •Persist updates through delegated dependencies.
Keep use cases focused on orchestration, not storage details.
Immutable Update Pattern
When a use case updates nested metadata/collections:
- •map by stable identifiers (for example
toolCallId) - •copy only affected items
- •preserve untouched items as-is
- •persist once after building the final result
This keeps behavior deterministic and testable.
Error Handling Pattern
Use cases should not wrap errors into generic result objects.
Rules:
- •return values directly on success
- •throw only managed domain exceptions when the error is part of the business flow
- •do not catch and re-wrap unknown exceptions into another generic error
- •let infrastructure/unexpected exceptions bubble to the caller unless there is a clear domain mapping requirement
Quick Checklist
- •use case class has constructor-injected dependencies
- •
call(...)method is the single entry point - •guard clauses exist for missing/invalid input
- •no provider/UI state mutation in use case
- •side effects are delegated to repositories/other use cases
- •returns a domain value directly whenever possible
Common Mistakes
- •Putting business logic in providers/controllers instead of a use case.
- •Using
ref.read(...)inside the use case instead of constructor injection. - •Catching broad exceptions and embedding them into generic failure objects.
- •Performing many small persistence writes instead of one aggregated update.
- •Mixing orchestration with formatting/presentation concerns.
Using Use Cases in the App Layer
Use case usage pattern in app modules:
- •instantiate use cases in an application controller/service, not in widgets
- •read repositories from provider/container, then compose use cases explicitly
- •call small use cases in sequence to form one user flow
- •keep provider/controller state focused on UI/runtime tracking only
Reference implementation:
- •composition root and flow orchestration:
apps/auravibes_app/lib/providers/tool_execution_controller.dart - •provider delegation entrypoints:
apps/auravibes_app/lib/providers/tool_calling_manager_provider.dart
Composition Root Pattern
Inside a controller method:
- •Read dependencies from providers/repository providers.
- •Build foundational use cases first (for example metadata update/checks).
- •Inject them into higher-level orchestrator use cases.
- •Execute the flow through use case
call(...)methods.
This keeps wiring explicit, testable, and easy to refactor.
Runtime Usage Flows
- •
runTask(...): create permission/check/update use cases -> filter allowed work -> execute batch -> continue next step only when pending work is resolved. - •
grantToolCall(...): build grant + execution use cases -> grant permission -> execute -> continue flow. - •
skipToolCall(...)/ stop flows: update metadata through dedicated update use case, then continue orchestration as needed.
App-Layer Do / Don't
Do:
- •compose use cases per method/flow in the app layer
- •pass IDs and domain inputs into use case
call(...) - •let use cases coordinate business operations
Don't:
- •instantiate repository clients directly inside widgets
- •mix UI rendering logic into use cases
- •bypass use cases by duplicating orchestration across providers/controllers
How to Use It
Prompt examples:
- •"Create a new domain use case using the usecase-pattern skill."
- •"Refactor this provider logic into a constructor-injected use case with guard clauses."
- •"Apply immutable update and managed domain exception rules in this use case."
During implementation, follow this order:
- •Define input/output contracts and any managed domain exceptions.
- •Write the use case class and constructor dependencies.
- •Add guard clauses and orchestration flow.
- •Delegate persistence/external calls.
- •Add unit tests for success paths and managed error paths.