skip to content

In Flutter's architecture guide, how does a service differ from a repository, and why do repositories return domain models instead of API models?

level: middleimportance: should knowfreq 40%

answer

  1. one service per data source
  2. no state in services
  3. Future and Stream responses
  4. private service field
  5. only the data the app needs

basics

~20 s

A service wraps one external data source and holds no state; a repository owns one data type, calling services and adding caching and retry. Repositories return domain models so view models never depend on raw API shapes.

solid answer

~40 s

In the guide, a **service** is the lowest layer: one class per data source (a REST API, local files, a platform plugin) that returns `Future`s or `Stream`s and holds no state. A **repository** sits above it as the source of truth for one data type; it can read several services, and one service can feed several repositories. The service is kept in a **private field** so the UI cannot bypass the repository. Services return **API models** in the shape the server sends; repositories convert them into **domain models** that hold only what the app needs, so a changed payload is fixed in one mapping rather than in every view model. The guide marks separate API and domain models as conditional: extra verbosity worth paying mainly in large apps.

code

dart · 41 lines
dart
// API model: mirrors the server payload.
class FlightOfferApiModel {
  const FlightOfferApiModel({
    required this.id,
    required this.carrierCode,
    required this.priceCents,
    required this.fareRulesRef,
  });
  final String id;
  final String carrierCode;
  final int priceCents;
  final String fareRulesRef;
}

// Domain model: only what the app needs.
class FlightOffer {
  const FlightOffer({required this.id, required this.carrier, required this.price});
  final String id;
  final String carrier;
  final double price;
}

// Service: one data source, no state.
abstract class FlightApiService {
  Future<List<FlightOfferApiModel>> searchOffers(String route);
}

class FlightRepository {
  FlightRepository({required FlightApiService api}) : _api = api;

  // Private: the UI layer cannot bypass the repository.
  final FlightApiService _api;

  Future<List<FlightOffer>> search(String route) async {
    final raw = await _api.searchOffers(route);
    return [
      for (final o in raw)
        FlightOffer(id: o.id, carrier: o.carrierCode, price: o.priceCents / 100),
    ];
  }
}

go deeper

for a junior

Remember: services wrap external sources, repositories own data and hand out domain models.

for a middle

Explain the many-to-many relationship, the private service field, and what a domain model drops compared with the payload.

for a senior

Argue when separate API and domain models pay off, and show how the mapping localizes the damage of a backend payload change.

for a principal

Weigh the verbosity and build time of generated models across a large codebase against looser coupling to the backend.

## Two data-layer classes with different jobs Flutter's architecture guide puts two classes in the **data layer**, and they are easy to blur: | | Service | Repository | |---|---|---| | Scope | One external data source | One type of app data | | State | None | Caches, session state | | Output | Raw responses as `Future` / `Stream` | Domain models | | Logic | Wrap the endpoint | Caching, retry, error handling, refresh, mapping | | Relationship | Many-to-many with repositories | Many-to-many with view models | A **service** exists to isolate data loading. The guide's rule of thumb is that services help most when the data lives outside your app's Dart code: a REST endpoint, the iOS or Android APIs behind a plugin, or files on disk. Keep **one service per data source**. A **repository** is the **source of truth** for a data type and the only place that data is changed. It calls one or more services, then adds the business logic around them. ## A flight-search data layer In a flight-search app: - `FlightApiService` wraps the fares HTTP API and returns `FlightOfferApiModel` objects. - `SavedSearchStorageService` wraps a local file of recent searches. - `FlightRepository` reads `FlightApiService`; `SearchHistoryRepository` reads `SavedSearchStorageService`. - If the airline list comes from the same HTTP API, a third repository can read `FlightApiService` too: services and repositories are many-to-many. ## Why the service is private The guide's sample stores the service in a private field of the repository. If it were public, a view model could call the API directly and skip the cache, the retry and the mapping, and the repository would stop being the source of truth. The same reasoning makes the repositories private inside a view model. ## API models versus domain models An **API model** mirrors the server's payload: every field, the server's naming, nested references to other resources. A **domain model** holds only what the app needs, already combined and cleaned. The guide's sample repository builds one `Booking` domain model from several API calls (the booking, its destination and its activities). Why convert: 1. **Change isolation.** When the API renames `price_cents` or splits a field, one repository mapping changes, not every view model that shows a price. 2. **Simpler view models.** They receive ready-to-use objects instead of filtering and joining raw payloads. 3. **Several sources, one model.** A domain model can merge a remote response with locally stored data. The cost is extra classes and mapping code. The guide's recommendations list marks separate API and domain models as **conditional, for large apps**, and immutable data models as **strongly** recommended; it names code generators such as freezed or built_value for equality and copy methods, while warning they add build time. ## Where the rules bend - A small app may let a repository return the API model directly while it is still identical to what the UI needs. - The guide recommends, but does not require, returning a `Result` type from services; error-wrapping is a separate topic. - A service may be a thin wrapper around a plugin; it still belongs in a class so the repository can be tested with a fake service.

  • Can one service feed several repositories?
    Yes. The guide makes services and repositories many-to-many: one HTTP service can supply both a flight repository and an airline repository, and one repository can merge a remote service with a local storage service.
  • Where would offline caching of search results go?
    In the repository. The guide gives repositories caching and, when an app works offline, synchronizing local and remote data; the local file or database is wrapped by its own service that the repository reads alongside the API service.

saying these in an interview costs you the question

  • Services should keep the cached data so repositories can stay stateless.
  • View models should use API model classes directly to avoid mapping.
  • Each repository must have exactly one service of its own.
  • Services are only for REST; files and plugins go straight into repositories.
  • Making the service field public is harmless because Dart is single-threaded.