Skip to content

Guides

Flutter monorepo with Melos: structure and setup ​

Short answer. A Flutter monorepo is one Git repository with many Dart packages: your app plus packages for features and shared code. Dart's pub workspaces resolve all of them together, with one pubspec.lock. Melos runs commands across them. Melos 7 keeps its configuration in the root pubspec.yaml, so you no longer need a melos.yaml.

This guide is about setup: the files you create and the commands you run. If you want to know how large apps decide what goes into a package, read Flutter monorepo architecture. This guide covers the workspace. For the folders and packages inside one app, read Flutter project structure: feature-first.

Is a monorepo worth it for you? ​

A monorepo costs you more files and a longer first setup. It pays off when:

  • Several people work on one app, and you want their features to stay out of each other's way.
  • You want shared code (UI kit, networking, config) to have clear owners and clear boundaries.
  • You run tests, analysis or builds for only the packages that changed.

For a small app with one developer, one package is fine. You can move to a workspace later: packages can start as folders and become packages when a boundary matters.

The layout ​

text
my_app/
├── pubspec.yaml            # workspace root + Melos config
├── pubspec.lock            # one lock file for everything
├── apps/
│   └── my_app/             # the Flutter app: wiring only
├── features/
│   ├── auth/
│   └── home/
└── packages/
    ├── core/               # networking, logging, helpers
    └── ui_kit/             # theme and shared widgets

The app depends on features. Features depend on core and ui_kit. Nothing depends on the app. This one-way flow is the most useful rule in the whole setup. Keep it from day one.

Step 1: Create the workspace root ​

The root pubspec.yaml lists every package. It is not an app.

yaml
name: my_workspace
publish_to: none

environment:
  sdk: ^3.9.0

workspace:
  - apps/my_app
  - features/auth
  - features/home
  - packages/core
  - packages/ui_kit

dev_dependencies:
  melos: ^7.0.0

Pub workspaces need Dart 3.6 or newer. Melos 7 needs a recent SDK to work reliably, and its docs recommend 3.9 or newer. If your SDK is 3.11 or newer, you can write apps/* instead of listing each package. Check the version with dart --version.

Step 2: Mark each package as a workspace member ​

In every package's pubspec.yaml, add resolution: workspace. Packages that depend on each other can use path dependencies:

yaml
# features/home/pubspec.yaml
name: home
publish_to: none

environment:
  sdk: ^3.9.0

resolution: workspace

dependencies:
  flutter:
    sdk: flutter
  core:
    path: ../../packages/core
  ui_kit:
    path: ../../packages/ui_kit

Then resolve everything at once from anywhere in the repo:

bash
flutter pub get

You get one pubspec.lock and one .dart_tool/package_config.json at the root. Every package uses the same version of every dependency. This is the main reason to use a workspace: no more "package A uses http 1.1 and package B uses 1.2".

Step 3: Add Melos scripts ​

Melos goes in dev_dependencies above, so you can run it with dart run melos. Many people also activate it globally (dart pub global activate melos) and type melos. Put the config in the root pubspec.yaml:

yaml
melos:
  scripts:
    analyze:
      run: dart run melos exec -c 4 -- flutter analyze
      description: Analyze every package.

    test:
      run: dart run melos exec -c 4 --dir-exists=test -- flutter test
      description: Test every package that has a test folder.

    format:
      run: dart run melos exec -- dart format .
      description: Format every package.

    generate:
      run: dart run melos exec -c 1 --depends-on=build_runner -- dart run build_runner build --delete-conflicting-outputs
      description: Run build_runner in packages that use it.

Run a script by name:

bash
dart run melos bootstrap
dart run melos run analyze
dart run melos run test

melos exec runs one command in every package. -c 4 runs four at a time. --dir-exists=test skips packages without tests. --depends-on=build_runner selects only packages that depend on build_runner.

Step 4: Run commands on a subset ​

Filters are the reason people use Melos instead of a shell loop. Add them to melos exec or melos run. The examples use melos. Write dart run melos if you did not activate it globally.

bash
# Only packages whose name matches a glob
melos exec --scope="home*" -- flutter test

# Only packages that depend on core
melos exec --depends-on=core -- flutter test

# Only packages that changed between origin/main and HEAD
melos exec --diff=origin/main...HEAD -- flutter test

# Changed packages plus the packages that depend on them
melos exec --diff=origin/main...HEAD --include-dependents -- flutter test

The last one is the safe choice for CI. If you change core, every package that uses core can break, so its tests must run too. Only test what changed goes deeper into this.

Step 5: Run it in CI ​

A minimal GitHub Actions job:

yaml
name: CI
on:
  pull_request:

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: subosito/flutter-action@v2
        with:
          channel: stable
          cache: true
      - run: flutter pub get
      - run: dart run melos run analyze
      - run: dart run melos run test

fetch-depth: 0 downloads the full history. You need it as soon as you compare against another branch with --diff.

Rules that keep a monorepo healthy ​

  • Dependencies point one way. App to features, features to shared packages, never the other direction.
  • Features do not import each other. If two features need the same thing, move it down into a shared package.
  • One owner per package. A package named utils that anyone can add to becomes a dumping ground.
  • Keep the app thin. It should wire packages together, not hold business logic.
  • Check it with a tool. A rule that nobody checks is a rule that breaks. Run a dependency check in CI.

Common problems ​

  • Old tutorials show a melos.yaml and pubspec_overrides.yaml. That is Melos 6 and earlier. Melos 7 uses pub workspaces. The upgrade steps are in the migration guide of the Melos docs.
  • pub get fails for a package. Every package with resolution: workspace must also be listed in the root workspace: list, and the other way round.
  • Different SDK constraints. Every workspace package needs an SDK constraint that works with the workspace. Keep them the same.
  • Slow melos exec. Set the concurrency with -c. Steps that must run in order, like build_runner in packages that depend on each other, need -c 1 and --order-dependents.
  • Hot reload and IDEs. Open the root folder in your IDE so the Dart analysis server sees the workspace as one project.

How Archipelago does this ​

archipelago create generates this whole setup, then fills it in.

  • The root pubspec.yaml is the workspace root. It lists packages by folder (apps/*, features/**, shared/*, infrastructure/* and others) and holds the Melos scripts. There is no melos.yaml.
  • The packages are split in layers: apps/ for the host app, features/ for business logic, infrastructure/ for SDK wrappers, shared/ and utilities/ for the code below them. Dependencies flow downward only.
  • The generated Melos scripts include analyze, test, format and code generation. melos bootstrap also seeds the development environment file after it runs.
  • A devtools/ folder holds the monorepo_toolkit. Its graph command finds cycles and checks the layer rules (graph validate), so the "dependencies point one way" rule is checked in CI instead of only written down. The toolkit lives in your repo and runs without the Archipelago CLI.
  • archipelago generate <brick> adds a new feature package that already fits the workspace.

References: Architecture, Monorepo Toolkit, Dependency Graph and the flutter_modular_monorepo brick.

FAQ ​

Do I need Melos if I have Dart workspaces? ​

Workspaces solve dependency resolution. They do not run commands across packages. Melos adds scripts, filters (--scope, --diff, --depends-on) and ordered execution. You can use workspaces without Melos if a shell loop is enough.

Do I have to publish my packages? ​

No. Set publish_to: none and use path dependencies. Workspace packages in an app repo are usually private.

How many packages is too many? ​

A package is worth it when it has a clear owner and a clear boundary. If you create a package for every folder, the extra files cost more than they save. Start with fewer and split when a boundary hurts.

Can I add an existing app to a workspace? ​

Yes. Move it under apps/, add resolution: workspace, list it in the root workspace:, and run flutter pub get. Then move shared code out into packages step by step.

Does it work with state management packages like bloc or riverpod? ​

Yes. Packages are independent of state management. Pick one and use it the same way in every feature.

Try Archipelago

Make a free account, install the CLI, and generate a project:

dart pub global activate archipelago_cli
archipelago auth login --interactive
archipelago create --output ./my_app

The Free plan includes 3 project scaffolds per month and the Free bricks. See Getting Started for each step, and pricing for Pro and Enterprise.