---
name: layered-architecture
description: >
  VGV layered monorepo architecture in Flutter: four layers Data, Repository, Business Logic, and
  Presentation, unidirectional dependency rules, and model transformation across layers. Use when
  structuring a multi-package Flutter app, creating data or repository packages, defining layer
  boundaries, or wiring packages in app bootstrap via path dependencies, barrel exports, and
  RepositoryProvider. Use too when asked to put a domain model or shared class in an api_client
  or data package, or to add a cross-package dependency the layers forbid. Also use for a new app
  described in one line ("I'm starting a weather app that reads from a REST API") plus a request
  for the package, project, folder, or directory layout, or for where a file or feature should
  live and which package code belongs in. Applies even when the user never says monorepo, layer,
  or architecture.
allowed-tools: Read Glob Grep mcp__very-good-cli__create mcp__very-good-cli__packages_get mcp__very-good-cli__test
effort: high
---

# Layered Architecture

Layered monorepo architecture for Flutter apps — four layers organized as independent Dart packages with strict unidirectional dependencies.

---

## Core Standards

Apply these standards to all layered architecture work:

- **Four layers** — Data, Repository, Business Logic, Presentation — a feature spans all four whenever its repository reads an external source
- **Unidirectional dependencies** — Presentation → Business Logic → Repository → Data — never skip or invert a layer
- **Data and Repository layers live in `packages/`** — each is an independent Dart package with its own `pubspec.yaml`
- **Business Logic and Presentation live in `lib/`** — organized by feature within the app
- **Data layer packages contain zero domain/business logic** — they must be reusable in unrelated projects
- **No inter-repository dependencies** — repositories never import other repositories
- **No Flutter SDK in data or repository packages** — scaffold with the `very_good_cli` MCP server `create dart_package` tool
- **One repository per domain** — `user_repository`, `weather_repository`, `auth_repository`
- **Path dependencies for local packages** — never `git:` or pub version references for packages in the same repo
- **Barrel exports at every package boundary** — `src/` is never imported directly by consumers
- **Repositories take every external source through the constructor** — a data client or an SDK object such as `FirebaseAuth.instance`, built in bootstrap and passed in, never constructed or defaulted inside the repository
- **App bootstrap wires all layers** — `main_<flavor>.dart` creates clients and repositories, provides them via `RepositoryProvider`
- **Only the app's entrypoints and bootstrap import a data package** — blocs, widgets, and app tests reach data through a repository
- **Dart 3.13 primary constructors** — on a Dart 3.13+ baseline, declare model and widget fields as primary-constructor declaring parameters (`class const User(final String id, final String name) extends Equatable`) rather than `this.field`; keep the classic form only below 3.13

> **Cross-harness fallback.** This skill scaffolds and tests packages via the Very Good CLI MCP server. On a host without this plugin's Bash hooks and without that MCP server connected, run the equivalent `very_good create dart_package …`, `very_good packages get`, and `very_good test` commands directly.

## Architecture Overview

| Layer | Responsibility | Location | Depends On | Example |
| --- | --- | --- | --- | --- |
| **Data** | External communication — API calls, local storage, platform plugins | `packages/<name>_api_client/` | External packages only | `user_api_client`, `local_storage_client` |
| **Repository** | Data orchestration — combines data sources, transforms models, caches | `packages/<name>_repository/` | Zero or more data layer packages | `user_repository`, `weather_repository` |
| **Business Logic** | State management — processes user actions, emits state changes | `lib/<feature>/bloc/` or `lib/<feature>/cubit/` | Repository layer | `LoginBloc`, `ProfileCubit` |
| **Presentation** | UI — widgets, pages, views, layout | `lib/<feature>/view/` | Business Logic layer | `LoginPage`, `ProfileView` |

```text
┌─────────────────────────────────────────────┐
│              Presentation                   │
│          (lib/<feature>/view/)              │
└──────────────────┬──────────────────────────┘
                   │ reads state / dispatches events
┌──────────────────▼──────────────────────────┐
│            Business Logic                   │
│        (lib/<feature>/bloc/)                │
└──────────────────┬──────────────────────────┘
                   │ calls repository methods
┌──────────────────▼──────────────────────────┐
│              Repository                     │
│      (packages/<name>_repository/)          │
└──────────────────┬──────────────────────────┘
                   │ calls data clients
┌──────────────────▼──────────────────────────┐
│               Data                          │
│     (packages/<name>_api_client/)           │
└─────────────────────────────────────────────┘
```

## Monorepo Structure

Features live in `lib/`, one directory each, split `bloc|cubit/` and `view/` with a barrel
file. Layers live in `packages/`, one package per data source and one per repository. The
second data package and the second repository follow the same shape as the first.

```text
my_app/
├── lib/
│   ├── app/
│   │   ├── app.dart                          # Barrel file
│   │   └── view/
│   │       └── app.dart                      # App widget with MultiRepositoryProvider
│   ├── login/                                # Feature: login
│   │   ├── login.dart                        # Barrel file
│   │   ├── bloc/
│   │   │   ├── login_bloc.dart
│   │   │   ├── login_event.dart
│   │   │   └── login_state.dart
│   │   └── view/
│   │       ├── login_page.dart               # Page provides Bloc
│   │       └── login_view.dart               # View consumes state
│   ├── profile/                              # Feature: profile
│   ├── main_development.dart                 # Flavor entrypoint
│   ├── main_staging.dart
│   └── main_production.dart
├── packages/
│   ├── auth_api_client/                      # Data layer: auth API
│   │   ├── lib/
│   │   │   ├── auth_api_client.dart          # Barrel file
│   │   │   └── src/
│   │   │       ├── auth_api_client.dart
│   │   │       └── models/
│   │   │           ├── models.dart
│   │   │           └── auth_response.dart
│   │   └── pubspec.yaml
│   ├── local_storage_client/                 # Data layer: local storage
│   ├── auth_repository/                      # Repository layer: auth
│   │   ├── lib/
│   │   │   ├── auth_repository.dart          # Barrel file
│   │   │   └── src/
│   │   │       ├── auth_repository.dart
│   │   │       └── models/
│   │   │           ├── models.dart
│   │   │           └── user.dart             # Domain model
│   │   └── pubspec.yaml
│   └── user_repository/                      # Repository layer: user
├── test/
│   └── ...                                   # Mirrors lib/ structure
└── pubspec.yaml                              # Root app pubspec
```

## Data Layer

The data layer handles all external communication. Each data package wraps a single external source (REST API, local database, platform plugin) and exposes typed methods and response models.

**Rules:**
- Models represent the external data shape — match the API/storage schema exactly
- No Flutter imports — use the `very_good_cli` MCP server `create dart_package` tool
- Constructor-inject HTTP clients for testability
- Response models use `fromJson` / `toJson` factories
- Export everything through a barrel file — never expose `src/`

### Pattern: Data Client Class

Constructor-inject the HTTP client for testability. Return typed response models — never raw JSON.

```dart
/// HTTP client for the User API.
class UserApiClient {
  // http.Client injected — tests pass a mock, production gets a real client
  UserApiClient({
    required String baseUrl,
    http.Client? httpClient,
  })  : _baseUrl = baseUrl,
        _httpClient = httpClient ?? http.Client();

  final String _baseUrl;
  final http.Client _httpClient;

  /// Every method returns a typed response model.
  Future<UserResponse> getUser(String userId) async {
    final response = await _httpClient.get(
      Uri.parse('$_baseUrl/users/$userId'),
    );
    if (response.statusCode != 200) {
      throw UserApiException(response.statusCode, response.body);
    }
    return UserResponse.fromJson(
      json.decode(response.body) as Map<String, dynamic>,
    );
  }
}
```

See [worked-example.md](references/worked-example.md) for the complete `user_api_client` package with pubspec, barrel files, response models, and exception class.

## Repository Layer

The repository layer orchestrates data sources and exposes domain models. Each repository composes the data clients it needs, transforms response models into domain models, and provides a clean API for the business logic layer.

**Rules:**
- No inter-repository dependencies — repositories are isolated
- No Flutter SDK — the `very_good_cli` MCP server `create dart_package` tool
- Domain models live in the repository package — not in data packages
- Transform data models into domain models — never leak API response shapes upstream
- A repository with no external source, such as in-memory session state, takes no constructor arguments

### Pattern: Domain Model + Repository Transformation

Domain models extend `Equatable` and represent the app's internal data shape — distinct from the API response shape. The repository method transforms between them.

```dart
/// Domain model — lives in the repository package, NOT the data package.
/// Fields match the app's needs, not the API schema.
class const User({
  required final String id,
  required final String email,
  required final String displayName,
  final String? avatarUrl,
}) extends Equatable {
  @override
  List<Object?> get props => [id, email, displayName, avatarUrl];
}
```

```dart
/// Repository accepts data client via constructor — never creates its own.
class UserRepository {
  const UserRepository({
    required UserApiClient userApiClient,
  }) : _userApiClient = userApiClient;

  final UserApiClient _userApiClient;

  /// Transforms UserResponse (API shape) → User (domain shape).
  Future<User> getUser(String userId) async {
    final response = await _userApiClient.getUser(userId);
    return User(
      id: response.id,
      email: response.email,
      displayName: response.displayName,
      avatarUrl: response.avatarUrl,
    );
  }
}
```

See [worked-example.md](references/worked-example.md) for the complete `user_repository` package with pubspec, barrel files, and error handling. See [model-transformation.md](references/model-transformation.md) for detailed transformation patterns between data and domain models.

## Dependency Graph

Path dependencies in each `pubspec.yaml` point one direction. A data package declares
external packages only. A repository package declares a path dependency on its data
packages. The root app declares its repository packages and every data package its bootstrap
constructs. `main_<flavor>.dart` imports each client to inject it, and
`depend_on_referenced_packages` in `package:very_good_analysis` requires every imported
package to be declared.

**Imports hold the layer boundary.** Once the app declares a data package, the lint no
longer stops a bloc from importing it. The boundary is the import rule in Core Standards:
only the app's entrypoints and bootstrap import a data package.

```yaml
# packages/user_api_client/pubspec.yaml — external packages only
dependencies:
  http: ^1.4.0

# packages/user_repository/pubspec.yaml — path dependency on its data package
dependencies:
  user_api_client:
    path: ../user_api_client

# pubspec.yaml: repositories, plus the data packages bootstrap constructs
dependencies:
  user_api_client:
    path: packages/user_api_client
  user_repository:
    path: packages/user_repository
```

See [references/pubspec.md](references/pubspec.md) for the three files in full and for
checking the import boundary, including which files count as entrypoints and bootstrap.

## Data Flow

Presentation dispatches an event → Bloc calls the repository → repository calls the data client and returns a domain model → `BlocBuilder` rebuilds on the new state.

See [references/data-flow.md](references/data-flow.md) for the code at each layer.

## App Bootstrap

`main_<flavor>.dart` imports and constructs every data client and repository, then passes
the repositories to the `App` widget, which exposes them through `MultiRepositoryProvider`.
Flavors change only configuration — base URLs, API keys — never the wiring shape.

```dart
// lib/main_development.dart
import 'package:auth_api_client/auth_api_client.dart';
import 'package:auth_repository/auth_repository.dart';
import 'package:flutter/material.dart';
import 'package:my_app/app/app.dart';
import 'package:user_api_client/user_api_client.dart';
import 'package:user_repository/user_repository.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  const baseUrl = 'https://api.dev.example.com';

  // Data layer
  final authApiClient = AuthApiClient(baseUrl: baseUrl);
  final userApiClient = UserApiClient(baseUrl: baseUrl);

  // Repository layer
  final authRepository = AuthRepository(authApiClient: authApiClient);
  final userRepository = UserRepository(userApiClient: userApiClient);

  runApp(
    App(
      authRepository: authRepository,
      userRepository: userRepository,
    ),
  );
}
```

See [references/worked-example.md](references/worked-example.md) for the full `main()` and
the `App` widget with `MultiRepositoryProvider`.

## Anti-Patterns

| Anti-Pattern | Problem | Correct Approach |
| --- | --- | --- |
| Widget calls API client directly | Bypasses Repository and Business Logic layers — no transformation, no state management | Widget dispatches event → Bloc calls Repository → Repository calls API client |
| Repository imports another repository | Creates circular or tangled dependency graphs — breaks independent testability | Each repository is self-contained; combine data at the Bloc level if needed |
| Domain models in data layer | Couples external API shape to internal domain — API changes break the entire app | Data layer has response models; Repository layer has domain models with transformation |
| Business logic in repository | Repository becomes untestable monolith mixing orchestration with rules | Repository transforms data; Bloc/Cubit contains all business rules |
| `git:` or pub version for local packages | Breaks monorepo — changes require publish/push cycles instead of instant local edits | Use `path:` dependencies for all packages within the monorepo |
| Flutter imports in data/repository packages | Prevents packages from being used in Dart-only contexts (CLI tools, servers) | Scaffold with the `very_good_cli` MCP server `create dart_package` tool — no Flutter SDK dependency |
| One giant repository for everything | God-object with too many responsibilities — impossible to test in isolation | One repository per domain boundary (`user_repository`, `settings_repository`) |
| Importing `src/` directly | Breaks encapsulation — consumers depend on internal structure | Export public API through barrel files; import the package, never `src/` paths |

## Common Workflows

### Adding a New Data Source

1. Scaffold the package with the `very_good_cli` MCP server `create dart_package` tool: `<name>_api_client --output-directory packages`
2. Add external dependencies to `pubspec.yaml` (e.g., `http`, `json_annotation`)
3. Create response models in `lib/src/models/` with `fromJson`/`toJson`
4. Create barrel file `lib/src/models/models.dart` exporting all models
5. Implement the client class in `lib/src/<name>_api_client.dart`
6. Create the package barrel file `lib/<name>_api_client.dart` exporting `src/` contents
7. Write unit tests in `test/` mirroring `lib/` structure — see the **testing** skill
8. Use `very_good_cli` MCP server tool `test` against the package directory — pass `directory: 'packages/<name>_api_client'` to scope the run

### Adding a New Repository

1. Scaffold the package with the `very_good_cli` MCP server `create dart_package` tool: `<name>_repository --output-directory packages`
2. Add path dependencies to data layer packages in `pubspec.yaml`
3. Add `equatable` to dependencies for domain models
4. Create domain models in `lib/src/models/` extending `Equatable`
5. Create barrel file `lib/src/models/models.dart`
6. Implement the repository class with constructor-injected data clients
7. Add transformation logic from response models to domain models
8. Create the package barrel file `lib/<name>_repository.dart`
9. Write unit tests with mocked data clients — see the **testing** skill

### Connecting a Repository to a Feature

1. Add path dependencies on the repository package and on each data package it takes to root `pubspec.yaml`
2. Create the data clients and the repository in `main_<flavor>.dart` and pass the repository to `App`
3. Add `RepositoryProvider.value` in `App`'s `MultiRepositoryProvider`
4. Create the Bloc/Cubit with the repository injected — see the **bloc** skill
5. Build the Page/View with `BlocProvider` and `BlocBuilder` — see the **bloc** skill
6. Grep `lib/` and `test/` for `package:<data_package>/` imports. The work is done when every hit is an entrypoint or bootstrap file, as the pubspec reference's Import Boundary section defines them

## Additional Resources

- [Complete worked example](references/worked-example.md) and [pubspec reference](references/pubspec.md)
- [Data flow walkthrough](references/data-flow.md) — the request path with code at each layer
- [Model transformation patterns](references/model-transformation.md) — data model vs domain model conversion
- [Package-level testing](references/testing.md) — testing data clients and repositories in isolation
- For Bloc/Cubit patterns and Page/View separation — see the **bloc** skill
- For project scaffolding use the `very_good_cli` MCP server `create dart_package` tool
- For testing data clients, repositories, and Blocs — see the **testing** skill
