In Flutter's architecture guide, when should you add the optional domain layer of use cases, and what dependency rules come with it?
answer
- optional, not a default
- merge, complexity, reuse
- add only when needed
- use cases depend on repositories
- view models keep repository access
basics
~20 sAdd use cases only for view model logic that merges several repositories, is very complex, or is reused by several view models. Use cases depend on repositories; view models may depend on both use cases and repositories.
solid answer
~40 sThe guide treats the **domain layer** as optional and marks it conditional in its recommendations: most apps do not need it. A **use case** (or interactor) earns its place when logic that would sit in a view model merges data from several repositories, is exceedingly complex, or is reused by several view models. The pros are less duplication, better testability and leaner view models; the cons are more classes, more mocks in tests and more boilerplate. The guide's preferred approach is to **add use cases as needed**: use cases depend on repositories (many-to-many), and a view model can depend on use cases *and* repositories directly. Forcing every data access through a use case is allowed but multiplies the overhead.
code
dart · 34 linesabstract class FlightRepository {
Future<int> legFareCents(String legId);
}
abstract class FareRulesRepository {
Future<int> bundleDiscountCents(List<String> legIds);
}
class ItineraryPrice {
const ItineraryPrice(this.totalCents);
final int totalCents;
}
// Domain layer: merges two repositories and is reused by the
// search results and checkout view models.
class PriceItineraryUseCase {
PriceItineraryUseCase({
required FlightRepository flights,
required FareRulesRepository fareRules,
}) : _flights = flights,
_fareRules = fareRules;
final FlightRepository _flights;
final FareRulesRepository _fareRules;
Future<ItineraryPrice> call(List<String> legIds) async {
var total = 0;
for (final id in legIds) {
total += await _flights.legFareCents(id);
}
total -= await _fareRules.bundleDiscountCents(legIds);
return ItineraryPrice(total);
}
}go deeper
Know that the domain layer exists, is optional, and holds use cases between view models and repositories.
List the three triggers and the dependency rules: use cases on repositories, view models on both.
Judge a real feature against the triggers, and explain the testing and boilerplate cost of adding use cases too early.
Set a team policy: add-as-needed versus use-case-only access, and the signal that says it is time to switch.
## What the domain layer is Flutter's architecture guide describes an app as a **UI layer** (views and view models) and a **data layer** (repositories and services). It then describes an **optional domain layer** in between, made of classes it calls **use cases** or **interactors**. A use case takes data from repositories and makes it suitable for the UI layer, so logic that would crowd a view model has its own home. The guide's recommendations page marks the domain layer as **conditional**: useful in apps with complex logic, but in most apps an unnecessary overhead. ## When a use case earns its place The guide lists three triggers. Logic that would otherwise sit in a view model moves into a use case when it: 1. **merges data from multiple repositories**, 2. **is exceedingly complex**, or 3. **is reused by different view models**. In a flight-search app, pricing a multi-city itinerary reads fares from `FlightRepository`, bundle discounts from `FareRulesRepository` and points from `LoyaltyRepository`, and both the search results and checkout screens show that price. All three triggers apply, so `PriceItineraryUseCase` is justified. Sorting one screen's results by departure time hits none of them and stays in its view model. ## The trade-off in the guide's own table | Pros | Cons | |---|---| | Avoids duplicated logic across view models | More classes and higher cognitive load | | Separates complex business logic from UI logic for testing | Tests need additional mocks | | Makes view models easier to read | More boilerplate | ## Access rules: add as needed The guide asks one design question: must view models reach data *only* through use cases, or may they still read repositories directly? It recommends **adding use cases only when needed**, with these rules: - use cases depend on repositories; - use cases and repositories have a **many-to-many** relationship; - a view model depends on one or more use cases **and** one or more repositories. The guide's picture is less a layered lasagna and more a **plated dinner**: two mains (UI and data) and a side (the domain layer). A team that later finds most view models already go through use cases can refactor to use-case-only access; starting there makes the code very modular and testable but adds a lot of overhead for simple reads. ## Rules that stay fixed Adding the layer does not relax the rest of the guide: - Repositories still never call each other; merging them is now the use case's job. - Use cases do not know about views, widgets or `BuildContext`. - A use case has well-defined inputs and outputs, so a view model test can replace it with a fake. - Views still talk only to their view model. ## What interviewers probe A weak answer treats use cases as mandatory ceremony, one per repository method. A strong one names the three triggers, admits the cost in classes and mocks, and describes the add-as-needed rules, including that a view model can use a repository directly next to a use case.
- Once one use case exists, must every view model switch to use-case-only access?No. Under the guide's add-as-needed approach a view model depends on use cases and repositories side by side. Use-case-only access is an option the guide describes, with more modularity and much more overhead; it suggests refactoring to it only if most access already goes through use cases.
- Can a use case call another repository's service directly for speed?No. Use cases depend on repositories, which remain the source of truth; reaching past them to a service skips caching and error handling and splits ownership of the data.
The guide's own picture: a plated dinner with two mains, the UI and data layers, and an optional side, the domain layer. You order the side when a dish calls for it; the mains are served either way, and a plate without the side is still a full meal.
saying these in an interview costs you the question
- Flutter's guide requires a use case for every repository call.
- Use cases should read view models to get the current UI state.
- Adding one use case forces every view model to stop using repositories.
- Use cases belong inside the data layer next to repositories.
- Repositories can call each other, so a use case is never needed.