From f7eff97324ec037328da8d620a5f58b9f556b73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Baudet?= Date: Wed, 21 Oct 2026 15:23:55 -0600 Subject: [PATCH] docs: re-sync FFCA reference with VGV Engineering Brings in VGVentures/very_good_engineering#293 (shared packages may depend on other shared packages) and #297 (lib/src/ layout on every page, the data barrel exports data sources but never DTOs or mappers, and packages import each other only through barrels), plus a wording change in the FAQ. The ffca-architecture and ffca-cross-feature skills now explain why a widget that needs a repository cannot live in ui_kit with the current rule: shared packages never depend on feature packages. The import checks cite the barrel and data barrel rules in Layer subfolders, now that the FFCA section states them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017Vxxv9fVFGupLXEnmx5bSq --- references/ffca/data.md | 2 +- references/ffca/faq.md | 2 +- references/ffca/presentation.md | 9 +++-- references/ffca/project_structure.md | 59 +++++++++++++++------------- scripts/validate_layers.dart | 17 ++++---- skills/ffca-architecture/SKILL.md | 2 +- skills/ffca-cross-feature/SKILL.md | 2 +- 7 files changed, 50 insertions(+), 43 deletions(-) diff --git a/references/ffca/data.md b/references/ffca/data.md index 92b59e3..a9f5859 100644 --- a/references/ffca/data.md +++ b/references/ffca/data.md @@ -14,7 +14,7 @@ The different storage mechanisms are known as _data sources_. The repository is For example, to enable some offline browsing, our online store app may have a Drift database data source and a Shopify http api data source. These live within the `data` package of a feature. -The mealify feature-first app does exactly this. Its `meals_data` package holds two data sources side by side: a [Drift database](https://github.com/VGVentures/mealify_feature_first/tree/main/features/meals/meals_data/lib/src/data_sources/meals_database) for local storage, and an [http client](https://github.com/VGVentures/mealify_feature_first/tree/main/features/meals/meals_data/lib/src/data_sources/mealdb_api_client) for the remote API. Neither is visible outside the package: the [repository](https://github.com/VGVentures/mealify_feature_first/blob/main/features/meals/meals_data/lib/src/repositories/meals_repository.dart) combines them, and the domain layer only ever sees a `Meal`. +The mealify feature-first app does exactly this. Its `meals_data` package holds two data sources side by side: a [Drift database](https://github.com/VGVentures/mealify_feature_first/tree/main/features/meals/meals_data/lib/src/data_sources/meals_database) for local storage, and an [http client](https://github.com/VGVentures/mealify_feature_first/tree/main/features/meals/meals_data/lib/src/data_sources/mealdb_api_client) for the remote API. The package's barrel exports both so the app can construct them and inject them into the repository, but it must not export DTOs or mappers. The [repository](https://github.com/VGVentures/mealify_feature_first/blob/main/features/meals/meals_data/lib/src/repositories/meals_repository.dart) converts the `ApiMeal` and `DbMeal` DTOs to a `Meal` object, and the other layers never see them. ## DTOs and mappers diff --git a/references/ffca/faq.md b/references/ffca/faq.md index a7bdb50..6fdc4b5 100644 --- a/references/ffca/faq.md +++ b/references/ffca/faq.md @@ -123,7 +123,7 @@ Notice that the `user_profile` feature depends on `auth_domain`, and `auth` know Generate a Dart package that knows how to talk to the Swagger client in the `shared` folder. As part of the package, it should start with a Dart script in the `tool` folder. The script should: 1. Download the latest version of the OpenAPI or Swagger spec. -2. Generate the client with a package like [swagger_to_dart](https://pub.dev/packages/swagger_to_dart). It uses [Dio](https://pub.dev/packages/dio), so you get the same HTTP client as the `api_client` recipe in the [Very Good Flutter Cookbook](https://github.com/VGVentures/very_good_flutter_cookbook). +2. Generate the client with a package like [swagger_to_dart](https://pub.dev/packages/swagger_to_dart), which uses [Dio](https://pub.dev/packages/dio) under the hood. 3. Export the parts each feature needs. Each feature then uses that shared client inside its own `data` package. Given a `/products/{id}` endpoint returning an `ApiProduct` DTO, `product_data` exposes a `ProductsRepository` with a `getProductById` method that: diff --git a/references/ffca/presentation.md b/references/ffca/presentation.md index 6b2a061..cb387b6 100644 --- a/references/ffca/presentation.md +++ b/references/ffca/presentation.md @@ -88,7 +88,7 @@ A widget that owns a `Cubit` is not a separate feature. It lives in its own feat `CartBadge` depends on `cart_domain`, its own feature's domain. What makes it worth discussing is that it renders inside a screen owned by a different feature. The question is not whether a widget may reach across domains. It is who places it, and what the dependency costs. ```dart -// cart_presentation/lib/cart_badge/bloc/cart_badge_cubit.dart +// cart_presentation/lib/src/cart_badge/bloc/cart_badge_cubit.dart class CartBadgeCubit extends Cubit { CartBadgeCubit({required ICartsRepository cartsRepository}) @@ -176,7 +176,7 @@ Use a direct import behind a narrow barrel when the widget is genuinely part of When the same widget is wanted in more than one feature, work through this in order. -1. **Does it need a repository or a domain type?** If not, it is a pure presentational component, and it belongs in `ui_kit`. This test is mechanical rather than stylistic. [Shared packages depend on external packages only](/architecture/ffca/project_structure/#dependency-rules), so a widget that needs a repository cannot compile there. +1. **Does it need a repository or a domain type?** If not, it is a pure presentational component, and it belongs in `ui_kit`. This test is mechanical rather than stylistic. [Shared packages never depend on feature packages](/architecture/ffca/project_structure/#dependency-rules), so a widget that needs a repository cannot compile there. 2. **If it does, split it.** `CartBadgeView`, taking a plain count, goes in `ui_kit`. `CartBadge`, the `Cubit` plus that view, stays in `cart_presentation` behind its own barrel. In practice, a widget that gets genuinely reused across features is usually a presentational leaf with the stateful part left behind, which means the reuse pressure was pointing at `ui_kit` all along. It becomes its own feature when it grows its own business logic, and a [presentation-only feature](/architecture/ffca/overview/#presentation-only-features) when it composes several features without owning business logic of its own. @@ -188,8 +188,9 @@ A single feature may expose multiple independent entry points. For example, `fav - features/favorites/ - favorites_presentation/ - lib/ - - favorites_list/ - - favorites_detail/ + - src/ + - favorites_list/ + - favorites_detail/ - favorites_list.dart (subfeature barrel) - favorites_detail.dart (subfeature barrel) - favorites_presentation.dart (primary barrel, re-exports all) diff --git a/references/ffca/project_structure.md b/references/ffca/project_structure.md index 8b920ce..67a5e33 100644 --- a/references/ffca/project_structure.md +++ b/references/ffca/project_structure.md @@ -20,12 +20,12 @@ This table is the whole rule, and it is the version a validation script implemen | `{feature}_domain` | shared packages, other features' `_domain` | | `{feature}_data` | its own `_domain`, other features' `_domain`, shared packages | | `{feature}_presentation` | its own `_domain`, other features' `_domain`, other features' `_presentation`, shared packages | -| `shared` | external packages only | +| `shared` | other shared packages, external packages | Two absences from that table carry as much weight as the rows themselves: - **No presentation package depends on any `_data` package**, its own included. The app wires the data layer implementation in. -- **No shared package depends on a feature package.** A shared package depends on external packages only, which is why a widget that needs a repository cannot live in `ui_kit`. +- **No shared package depends on a feature package.** A shared package may depend on other shared packages and external packages, but never on a feature package, which is why a widget that needs a repository cannot live in `ui_kit`. Cycles between packages are forbidden at every layer. If two packages each need something from the other, that shared part belongs in a third package underneath both of them. @@ -109,33 +109,36 @@ Data packages with a backend suffix, such as `auth_data_firebase`, signal the ba - product/ - product_domain/ Dart package - lib/ - - models/ - - product.dart - - repositories/ - - i_products_repository.dart + - src/ + - models/ + - product.dart + - repositories/ + - i_products_repository.dart - product_domain.dart barrel file - product_data/ Dart package - lib/ - - data_sources/ - - products_remote_data_source/ - - dtos/ - - product_dto.dart - - products_remote_data_source.dart - - mappers/ - - product_mapper.dart - - repositories/ - - products_repository.dart + - src/ + - data_sources/ + - products_remote_data_source/ + - dtos/ + - product_dto.dart + - products_remote_data_source.dart + - mappers/ + - product_mapper.dart + - repositories/ + - products_repository.dart - product_data.dart barrel file - product_presentation/ Flutter package - lib/ - - product_detail/ - - bloc/ - - views/ - - product_detail_module.dart - - product_list/ - - bloc/ - - views/ - - product_list_module.dart + - src/ + - product_detail/ + - bloc/ + - views/ + - product_detail_module.dart + - product_list/ + - bloc/ + - views/ + - product_list_module.dart - product_detail.dart subfeature barrel - product_list.dart subfeature barrel - product_presentation.dart primary barrel @@ -164,7 +167,7 @@ Data packages with a backend suffix, such as `auth_data_firebase`, signal the ba Each layer uses the same subfolders every time, so you always know where to look for something. -**Domain** (`{feature}_domain/lib/`): +**Domain** (`{feature}_domain/lib/src/`): | Folder | Contents | | --------------- | ----------------------------------------------------------------------------- | @@ -174,7 +177,7 @@ Each layer uses the same subfolders every time, so you always know where to look One note on naming: the folder keeps the conventional `use_cases/` name so the layout matches other clean architecture projects, even though we [name the classes themselves](/architecture/ffca/domain/#business-rules) Command and Query. -**Data** (`{feature}_data/lib/`): +**Data** (`{feature}_data/lib/src/`): | Folder | Contents | | --------------- | -------------------------------------------------------------------- | @@ -182,7 +185,7 @@ One note on naming: the folder keeps the conventional `use_cases/` name so the l | `mappers/` | Extension methods mapping DTOs and generated classes to domain models | | `repositories/` | Concrete repository implementations | -**Presentation** (`{feature}_presentation/lib/`): +**Presentation** (`{feature}_presentation/lib/src/`): | Folder | Contents | | --------------------------------------------- | --------------------------------------------------- | @@ -190,7 +193,9 @@ One note on naming: the folder keeps the conventional `use_cases/` name so the l | `{screen_name}/views/` | Screen and widget implementations | | `{screen_name}/{screen_name}_module.dart` | The module wiring dependencies for that screen | -Each layer has a primary barrel file at the root of `lib/`, named `{feature}_{layer}.dart`, plus [subfeature barrel files](/architecture/ffca/presentation/#subfeature-barrel-files) for any entry point that should be independently importable. +Each layer has a primary barrel file at the root of `lib/`, named `{feature}_{layer}.dart`, plus [subfeature barrel files](/architecture/ffca/presentation/#subfeature-barrel-files) for any entry point that should be independently importable. Everything else lives under `lib/src/`, so it stays private by default. Packages import each other only through the barrel files at the root of `lib/`, never a file inside `lib/src/`. See [barrel files](/architecture/barrel_files/) for the general rule. + +The data barrel exports the repository and the data sources, so the app can construct the data sources and inject them into the repository. It never exports DTOs or mappers. ## Monorepo tooling diff --git a/scripts/validate_layers.dart b/scripts/validate_layers.dart index f134359..0676d8c 100644 --- a/scripts/validate_layers.dart +++ b/scripts/validate_layers.dart @@ -502,13 +502,13 @@ List<_Violation> _checkOwnership(Package p, Workspace ws) { } /// Checks the package's barrels and its imports and exports of other -/// workspace packages. Implements the barrels in "Folder layout" and "Layer -/// subfolders" and the dedicated-barrel condition in "Dependency rules" of -/// references/ffca/project_structure.md, and "Widgets that own state" in -/// references/ffca/presentation.md ("never the primary barrel"). The general -/// rule that packages import each other only through barrels is on the VGV -/// Engineering barrel files page, which the FFCA section does not state yet -/// (VGVentures/very_good_engineering#294). Imports go through the same rules +/// workspace packages. Implements "Layer subfolders" in +/// references/ffca/project_structure.md (the primary barrel, packages import +/// each other only through the barrels at the root of lib/, and the data +/// barrel never exports DTOs or mappers), the dedicated-barrel condition in +/// its "Dependency rules", and "Widgets that own state" in +/// references/ffca/presentation.md ("never the primary barrel"). Imports go +/// through the same rules /// table as pubspec dependencies, since a workspace resolves an undeclared /// import of another member. In [incremental] mode a package with no lib/ yet /// is being scaffolded, so its missing barrel is not reported until --all. @@ -550,7 +550,8 @@ List<_Violation> _checkImports( for (final f in p.dartFiles) { // Any barrel at the root of a data package's lib/ that exports its own // DTOs or mappers, directly or through one re-exporting file. Data - // sources are exported: the app constructs them to inject them. + // sources are exported: the app constructs them to inject them ("Layer + // subfolders" in references/ffca/project_structure.md). final rootBarrel = f.libRel.startsWith('lib/') && !f.libRel.substring(4).contains('/'); if (p.layer == 'data' && rootBarrel) { diff --git a/skills/ffca-architecture/SKILL.md b/skills/ffca-architecture/SKILL.md index 2caba0c..f3fc0ad 100644 --- a/skills/ffca-architecture/SKILL.md +++ b/skills/ffca-architecture/SKILL.md @@ -59,7 +59,7 @@ These are the violations the architecture exists to prevent. Read the cited sect - Presentation importing a data layer. Depend on the domain and go through its repository interface. See `references/ffca/project_structure.md`, section *Dependency rules*. - A shared package depending on a feature. See `references/ffca/overview.md`, section *Shared libraries*. - Business logic living in `shared/`. Volatile, app-specific logic belongs in a feature domain. See the component table in the same section. -- A widget in `ui_kit` that needs a repository. It cannot compile there, because shared packages depend on external packages only. Split it: the presentational view goes to `ui_kit`, the stateful part stays in its feature. See `references/ffca/presentation.md`, section *Where the widget should live*. +- A widget in `ui_kit` that needs a repository. It cannot compile there, because shared packages never depend on feature packages. Split it: the presentational view goes to `ui_kit`, the stateful part stays in its feature. See `references/ffca/presentation.md`, section *Where the widget should live*. ## Where to go next diff --git a/skills/ffca-cross-feature/SKILL.md b/skills/ffca-cross-feature/SKILL.md index f963d4a..cdb5505 100644 --- a/skills/ffca-cross-feature/SKILL.md +++ b/skills/ffca-cross-feature/SKILL.md @@ -55,7 +55,7 @@ A widget that owns a `Cubit` is not a separate feature. It lives in its own feat When the same widget is wanted in more than one feature, work through `references/ffca/presentation.md`, section *Where the widget should live*, in order: -1. **Does it need a repository or a domain type?** If not, it is a pure presentational component and belongs in `ui_kit`. This test is mechanical, not stylistic: shared packages depend on external packages only, so a widget that needs a repository cannot compile there. +1. **Does it need a repository or a domain type?** If not, it is a pure presentational component and belongs in `ui_kit`. This test is mechanical, not stylistic: shared packages never depend on feature packages, so a widget that needs a repository cannot compile there. 2. **If it does, split it.** The view taking a plain count goes in `ui_kit`; the Cubit plus that view stays in its feature behind its own barrel. A widget that genuinely gets reused across features is usually a presentational leaf with the stateful part left behind, which means the pressure was pointing at `ui_kit` all along. ## When a Query is not enough: compose a feature