Modularisation: packages and melos
A folder split keeps the rule as a recommendation; a package split makes it mandatory. The difference in one sentence: if packages/domain's pubspec.yaml has no Flutter dependency, the line import 'package:flutter/material.dart' does not compile.
That is the strongest possible layer enforcement, but it has a price:
- A separate
pubspec.yamland version management per package. - Code generation (
build_runner) runs per package. - Past a couple of packages you need a tool to run commands across all of them — that is melos's role.
- Onboarding complexity rises for new team members.
When to split. Three concrete signals:
1. Code is shared: a mobile app plus an admin panel, or mobile plus server-side Dart. Domain models and validation rules must be identical on both sides. 2. Several teams: each team develops its own feature package independently, and review boundaries become clear. 3. Build time: code generation across the whole project takes minutes; once split, only the changed package regenerates.
If none of those signals is present, a folder split plus a grep check in CI is entirely sufficient.
# ══ Struktur ══
# my_app/
# ├── pubspec.yaml ← workspace + melos konfiqurasiyası
# ├── packages/
# │ ├── domain/ ← təmiz Dart: modellər, müqavilələr
# │ ├── data/ ← service, DTO, repository impl.
# │ ├── ui_kit/ ← paylaşılan widget-lər, tema
# │ ├── feature_orders/
# │ └── feature_loyalty/
# └── app/ ← giriş nöqtəsi, DI, routing
# ══ my_app/pubspec.yaml (root) ══
name: my_app_workspace
publish_to: none
environment:
sdk: ^3.6.0 # Pub workspace dəstəyi üçün minimum
# Pub workspace: paketlər bir `pub get` ilə həll olunur.
workspace:
- packages/domain
- packages/data
- packages/ui_kit
- packages/feature_orders
- packages/feature_loyalty
- app
# melos konfiqurasiyası — melos 7+ üçün eyni faylda.
dev_dependencies:
melos: ^8.0.0
melos:
scripts:
analyze:
run: dart analyze .
exec:
concurrency: 4
test:
run: flutter test
exec:
concurrency: 4
packageFilters:
dirExists: test # yalnız testi olan paketlərdə
generate:
run: dart run build_runner build --delete-conflicting-outputs
exec:
concurrency: 2
packageFilters:
dependsOn: build_runner
check-layers:
run: ../../tool/check_layers.sh
packageFilters:
scope: domain
# ══ packages/domain/pubspec.yaml ══
# name: domain
# resolution: workspace
# environment:
# sdk: ^3.6.0 ← `flutter:` bölməsi YOXDUR
# dependencies:
# freezed_annotation: ^3.0.0 (təmiz Dart)
# meta: ^1.15.0
# dev_dependencies:
# build_runner: ^2.4.0
# freezed: ^3.0.0
# test: ^1.25.0 (flutter_test DEYİL)
# ══ Əmrlər ══
# melos bootstrap → bütün paketlərin asılılıqlarını həll edir
# melos run analyze → hər paketdə `dart analyze`
# melos run test → yalnız testi olan paketlərdə `flutter test`
# melos run generate → yalnız build_runner-a ehtiyacı olanlarda
# melos exec -- <əmr> → ixtiyari əmri hər paketdə işlədirA monorepo setup: the root `pubspec.yaml` carries both the Pub workspace and the melos configuration (melos 7+ no longer uses a separate `melos.yaml`).
| Package | Flutter dependency | May import | Purpose |
|---|---|---|---|
| `domain` | ❌ None | Nobody (only pure Dart packages) | Models, contracts, `Failure`s, use cases |
| `data` | ⚠️ Possibly (for plugins) | `domain` | Services, DTOs, repository implementations |
| `ui_kit` | ✅ Yes | No feature (only Flutter) | Theme, shared widgets, formatters |
| `feature_*` | ✅ Yes | `domain`, `ui_kit` — not another feature | One feature's notifiers and screens |
| `app` | ✅ Yes | Everything | Entry point, DI graph, routing, flavors |
The rule of the package graph. The most important decision: feature packages do not import each other. Otherwise the split loses its value — a feature_orders → feature_catalog → feature_orders chain appears and no package builds independently.
Features relate to each other in two ways:
- Shared data → through the
domainanddatapackages. Bothfeature_ordersandfeature_cataloguseProductRepository. - Navigation → in the
apppackage. A feature only expresses the intent "go to the product screen for this id"; the routes are known toapp.
Splitting strategy: gradually. There is no need to split the whole project at once. The most useful order is usually:
1. `domain` — the step with the biggest payoff: the layer rule becomes compiler-enforced and its tests run under dart test.
2. `ui_kit` — theme and shared widgets; it pays off immediately when a second app (tablet, admin) arrives.
3. `data` — services and repository implementations.
4. *`feature_`** — only once team boundaries appear.
A practical note on code generation. After splitting, build_runner runs per package. melos run generate does that in one command, and packageFilters: dependsOn: build_runner selects only the packages that need it. That is also where the build-time win comes from: only the changed package regenerates.
A melos version nuance. From version 7, melos no longer uses a separate melos.yaml: the configuration lives in the root pubspec.yaml under a melos key, and package locations go in the workspace key (the Pub workspace mechanism, which requires Dart SDK 3.6.0+). Older examples you find online that show melos.yaml are for 6.x and earlier.
Practice (optional — only if the signals are present). Extract the domain layer of your project into its own package:
1. Create packages/domain/ with a pubspec.yaml that has no `flutter:` section.
2. Move lib/domain/* there (git mv).
3. Add domain: {path: packages/domain} to the main pubspec.yaml and fix the imports.
4. Run cd packages/domain && dart test — it must work without Flutter.
Done means: attempting to write a package:flutter import under packages/domain/lib makes the analyzer error (try it once and revert), and the domain tests run under dart test.
📚 Sources and documentation
- The melos packageofficialpub.dev
The `bootstrap`, `run`, `exec`, `version` and `publish` commands, and the `pubspec.yaml` configuration in 7+.
- Dart: creating packagesofficialdart.dev
A package's structure, its `pubspec.yaml`, and the `lib/src/` convention.
- Dart: Pub workspaces (monorepo)officialdart.dev
The `workspace` and `resolution: workspace` keys — what melos 7+ builds on.
- Case study: structure overviewofficialdocs.flutter.dev
For comparison: the official sample is a single package and settles for a folder split.