Sparround

Folder structure: feature-first, layer-first and the hybrid

Folder structure is not architecture, but it is the visible part of it — the team looks at it every day. There are three options.

Layer-first — layers at the top level: lib/domain, lib/data, lib/presentation, with features inside each layer.

  • Good: very simple in small and mid-size projects; the layer rule is right there in the tree.
  • Bad: changing one feature means walking through three separate folders; in a 30-screen app the presentation folder becomes enormous.

Feature-first — features at the top level: lib/features/orders/{domain,data,presentation}, each feature carrying its own layers.

  • Good: one folder is enough to work on a feature; deleting a feature or extracting it into a package is easy.
  • Bad: shared code becomes a problem. Which feature does AuthRepository belong to? When there is no answer, core/ turns into the place where everything is dumped.

Hybrid — the official Flutter guide's choice: UI split by feature, data split by layer. The reasoning is simple and convincing: a view and a view model belong to one feature, while repositories and services do not — several features use them.

text
lib/
├── ui/                        ← UI qatı: FEATURE-lərə görə
│   ├── core/
│   │   ├── ui/                ← paylaşılan widget-lər
│   │   └── themes/
│   └── <feature_adı>/
│       ├── view_models/
│       └── widgets/
├── domain/                    ← modellər (entity)
│   └── models/
├── data/                      ← data qatı: QAT üzrə
│   ├── repositories/
│   ├── services/
│   └── model/                 ← API modelləri (DTO)
├── config/
├── routing/
├── utils/
├── main.dart
├── main_development.dart      ← flavor-lar ayrı giriş nöqtələri
└── main_staging.dart

Diqqət: `/widgets` adlı qovluq yoxdur — paylaşılan widget-lər `ui/core/ui`
altındadır. Rəsmi tövsiyə budur: qovluq adı da qatı desin.

The structure of the official case study (Compass) app — a real example of the hybrid approach.

CriterionLayer-firstFeature-firstHybrid (official)
Working on one featureWalking through three foldersOne folderOne folder for the UI, shared data
A shared repositoryNatural — `data/` is sharedAwkward — `core/` bloatsNatural — data is split by layer
Navigating 30+ screensHeavy: an enormous `presentation/`ComfortableComfortable
Extracting a feature into a packageHardEasyMedium — the UI is easy, the data stays shared
Merge conflict riskHigh: everyone in the same foldersLowLow to medium
When to choose it< 10 screens, single developerA large project heading for modulesThe default for most projects

Beyond folders, two rules keep the structure readable.

1. Let the name state the layer. The official recommendation: a class name should announce its architectural role — HomeViewModel, UserRepository, ProductApiClient. Avoid names that collide with Flutter SDK types (Router, Route, State). And avoid catch-all suffixes — Manager, Helper, Util — they hide the layer and turn into the class where everything gets dumped.

2. Let `test/` mirror `lib/`. lib/data/repositories/order_repository_remote.darttest/data/repositories/order_repository_remote_test.dart. Then "does this class have a test?" is answered at a glance.

A short note on barrel files (a file of exports such as orders.dart): in a small project they shorten imports, but a barrel across a layer boundary hides the dependency rule — whoever writes import 'package:app/domain.dart' cannot see what they pulled in. Avoiding barrels at layer boundaries is the safer default.

Practice. Write your project's folder structure into an ARCHITECTURE.md: a tree diagram, one line describing each folder, and a note on what does not go there. Then place five of your existing files into that tree.

Done means: the file is in the repo, the tree clearly shows domain, data and the UI part, and at least one folder carries a "X does not go here, because …" line. This ends up being the document you reach for most often in code review.

📚 Sources and documentation