skip to content

In Flutter's architecture guide, when should you add the optional domain layer of use cases, and what dependency rules come with it?

level: seniorimportance: should knowfreq 30%

answer

  1. optional, not a default
  2. merge, complexity, reuse
  3. add only when needed
  4. use cases depend on repositories
  5. view models keep repository access

basics

~20 s

Add 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 s

The 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 lines
dart
abstract 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

for a junior

Know that the domain layer exists, is optional, and holds use cases between view models and repositories.

for a middle

List the three triggers and the dependency rules: use cases on repositories, view models on both.

for a senior

Judge a real feature against the triggers, and explain the testing and boilerplate cost of adding use cases too early.

for a principal

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.