Feature-First Clean Architecture
Feature-First Clean Architecture (FFCA) is our AI-ready monorepo architecture for Flutter. Every feature is a small set of Dart and Flutter packages, and your apps compose those features into something you can ship.
Why go to the trouble of making each layer a separate package, rather than a folder? Because a package boundary is enforced for you. If the cart feature isn’t allowed to reach into the product feature’s data layer, then that package isn’t in its pubspec.yaml, and the analyzer will say so before a reviewer has to. Furthermore, well-bounded packages give AI agents a deterministic target to write into, which reduces errors and makes their output consistent regardless of the model or the prompt.
FFCA is designed to:
- Give AI agents deterministic targets, so their output stays consistent regardless of model or prompt.
- Enable teams to ship features independently, without waiting on other teams.
- Establish ownership models for shared code.
- Allow features to be reused in different apps. For example, you could build a feature and use it for a point of sale app, a mobile app, and an admin app.
- Reduce friction when multiple teams touch the same codebase.
- Address a couple of shortcomings we’ve hit with our other architectural approaches, namely multiple blocs needing to perform the same data transformations, and the difficulty of sharing functionality across different blocs.
The structure
Section titled “The structure”Each project contains three major parts in a monorepo workspace: apps, features, and shared libraries. Apps depend on features and shared libraries, features depend on shared libraries, and nothing depends on an app.
---
config:
layout: elk
themeVariables:
fontSize: 20px
---
flowchart TB
subgraph apps["Apps"]
kiosk["kiosk_app"]
mobile["mobile_app"]
admin["admin_app"]
end
subgraph features["Features"]
subgraph product["product"]
product_domain["product_domain<br/>(Dart)"]
product_data["product_data<br/>(Dart)"]
product_presentation["product_presentation<br/>(Flutter)"]
end
subgraph cart["cart"]
cart_domain["cart_domain<br/>(Dart)"]
cart_data["cart_data<br/>(Dart)"]
cart_presentation["cart_presentation<br/>(Flutter)"]
end
subgraph auth["auth (headless)"]
auth_domain["auth_domain<br/>(Dart)"]
auth_data["auth_data_firebase<br/>(Dart)"]
end
subgraph analytics["analytics (headless)"]
analytics_domain["analytics_domain<br/>(Dart)"]
analytics_data["analytics_data_posthog<br/>(Dart)"]
end
end
subgraph shared["Shared"]
api_client["api_client<br/>(Dart)"]
ui_kit["ui_kit<br/>(Flutter)"]
localizations["localizations<br/>(Flutter)"]
logging["logging<br/>(headless feature)"]
end
apps --> features
apps --> shared
product_data --> product_domain
product_presentation --> product_domain
cart_data --> cart_domain
cart_presentation --> cart_domain
cart_domain --> product_domain
auth_data --> auth_domain
analytics_data --> analytics_domain
features --> shared
Apps are the deployable applications. In a large codebase, you might have several of them, such as a kiosk app, an admin app, and a mobile app.
In this architecture, the responsibility of the app is rather limited. It composes a series of features and shared libraries to build a complete, deployable artifact that your target audience can use. Each app then defines its own routing structure, environment configurations, bundle ids, and CI/CD workflows for verification and deployment.
Features
Section titled “Features”A feature is a cohesive unit of functionality. Some features have screens, some don’t. Every feature follows clean architecture, with domain, data, and optionally presentation layers as separate packages.
A feature with all three layers owns a user-facing flow. For example, an online store app may have a Product or Cart feature, to display a Product Screen or Shopping Cart.
Headless features
Section titled “Headless features”A feature without a presentation layer is a headless feature. It provides business logic and data access consumed by other features, but it owns no screens. Common examples include auth, analytics, and user_profile.
A headless feature can grow into a full feature by adding a presentation layer package. For example, auth starts as a headless feature with domain and data, and when you need a login screen, adding auth_presentation makes it a full feature. No structural changes to the existing packages are required.
Shared libraries
Section titled “Shared libraries”Applications and features may require common, shared libraries to achieve their goals. For example, many features may need a Swagger-generated api_client to perform HTTP requests, or a ui_kit (aka design system) for common widgets. Rather than each feature defining their own api_client, we can use a shared library across all features.
The distinction between shared libraries and features can be a bit fuzzy. A good rule of thumb: shared code should have zero knowledge of your app’s features. Shared libraries never import from a feature folder, and their API makes sense without any feature-specific context. If you find yourself referencing a particular feature from shared, that code likely belongs in the feature instead.
The decision rule comes down to two questions:
- Is it a business capability that apps compose? Then it belongs in
features/. - Could you publish it to pub.dev and would a stranger use it unchanged? Then it belongs in
shared/.
This is why business-domain packages like auth, analytics, and user_profile are features, often headless ones, rather than shared libraries. They represent business decisions: which auth provider to use, which analytics events to track, and what user data to store. Different apps have different backends. For example, some apps may use Firebase Auth while others use Auth0, and some apps may use Google Analytics while others use PostHog.
Infrastructure packages like logging and secure_storage can be either shared libraries, if they’re generic enough for any project, or headless features, if they’re customized for the project. A generic ILogger interface with a Sentry implementation belongs in shared/. A project-specific analytics setup with custom events belongs in features/.
| Component | Shared? | Why |
|---|---|---|
PrimaryButton | Yes | It is part of the brand design system, and agnostic of what it clicks. |
LoginButton | No | It implies a “Login” domain concept. Put it in auth_presentation. |
DioClient | Yes | It wraps generic HTTP logic, such as interceptors and timeouts. |
ProductApiClient | No | It knows about “Products” and API endpoints. Put it in product_data. |
formatCurrency() | Yes | Formats a double to a string. Generic. |
calculateTax() | No | Tax rules are volatile business logic. Put it in cart_domain. |
Feature layers
Section titled “Feature layers”Each feature is made up of three layers: the domain layer, the data layer, and the presentation layer, following the practices of clean architecture. Each of these layers is an individual Dart or Flutter package.
- The domain layer is the heart of the feature. It is pure Dart, and it defines your models, the business rules that operate on them, and the repository interfaces the feature needs.
- The data layer implements those repository interfaces. It owns the databases, the http clients, and the mapping from their DTOs onto your domain models.
- The presentation layer builds the UI, and defines the blocs that tie the domain layer to your widgets. A headless feature simply doesn’t have one.
Notice which way the arrows point. Both the data layer and the presentation layer depend on the domain, and the domain depends on neither of them. Therefore, you can swap Firebase for Auth0, or rebuild a screen from scratch, without touching the rules in the middle.
flowchart TB
subgraph Feature
direction TB
P["Presentation Layer<br/>(Flutter package)"]
D["Domain Layer<br/>(Dart package)"]
DA["Data Layer<br/>(Dart package)"]
P -->|"depends on"| D
DA -->|"depends on"| D
end
Where to go next
Section titled “Where to go next”Now that you know how a project is laid out, here’s the rest of the section, in the order the layers depend on each other:
- Domain covers models, Commands and Queries, repository interfaces, and how one feature combines information from another.
- Data covers data sources, DTOs, and mapping them onto your domain models.
- Presentation covers modules, localizations, and subfeature barrel files.
- Navigation covers how a feature moves the user elsewhere without importing another feature’s routes.
- Project structure covers naming conventions, the folder layout, and monorepo tooling.
- FAQ answers the questions that come up most often.