Sparround

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.yaml and 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.

yaml
# ══ 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ədir

A monorepo setup: the root `pubspec.yaml` carries both the Pub workspace and the melos configuration (melos 7+ no longer uses a separate `melos.yaml`).

PackageFlutter dependencyMay importPurpose
`domain`❌ NoneNobody (only pure Dart packages)Models, contracts, `Failure`s, use cases
`data`⚠️ Possibly (for plugins)`domain`Services, DTOs, repository implementations
`ui_kit`✅ YesNo feature (only Flutter)Theme, shared widgets, formatters
`feature_*`✅ Yes`domain`, `ui_kit` — not another featureOne feature's notifiers and screens
`app`✅ YesEverythingEntry 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_ordersfeature_catalogfeature_orders chain appears and no package builds independently.

Features relate to each other in two ways:

  • Shared data → through the domain and data packages. Both feature_orders and feature_catalog use ProductRepository.
  • Navigation → in the app package. A feature only expresses the intent "go to the product screen for this id"; the routes are known to app.

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.