You start with one Flutter app, then a web dashboard gets added, then a shared design system — and one day you notice you're trying to keep the same Dio version in sync across three separate repos. This is exactly where Flutter monorepo Melos usage comes in: it lets you manage multiple Dart/Flutter packages in a single repo with a single dependency resolution. In this post you'll see, step by step, how to set up Melos and fit it onto a real multi-package project, which scripts you need to define, and where you might trip up.
💡 Pro Tip: Before moving to Melos, count how many separate pubspec.yaml files your current project actually has, and how many of them really form an "internal dependency" relationship — if the answer fits on one hand, you probably don't need a monorepo yet, just a shared package.Table of Contents
- When to Move to a Monorepo (and When Not To)
- Typical monorepo candidates
- Melos Setup and Configuration Anatomy
- Drawing Package Boundaries: Feature, Core, Design System
- Dependency Alignment and Version Conflicts
- Scripts: Bootstrap, Analyze, Test, Format
- Automatic Versioning and Changelog via Conventional Commits
- Testing Only the Changed Package in CI
- Relationship to Pub Workspaces
- Migration Cost and Rollback
- FAQ
- How do you set up a monorepo in Flutter?
- What is Melos for?
- How do you manage multiple Flutter apps in the same repo?
- What's the difference between pub workspaces and Melos?
- Update (September 2026)
- Conclusion
- Sources
When to Move to a Monorepo (and When Not To)
Dart's official pub workspaces documentation clearly describes the concrete cost of keeping multiple packages in separate repos: you run dart pub get individually for each package, cross-package dependency versions drift apart over time, and a separate IDE analysis context per package increases memory usage. These aren't theoretical — they're measurable friction points, and moving to a monorepo eliminates exactly these three.
So when should you not move? There's no official answer here; this is my own experience talking. If your packages publish completely independently, move at different speeds under different teams, and share no real code, a monorepo gives you nothing but extra CI complexity. My simple test: if two packages see a change affecting each other within the same week, they belong in the same repo; if not, keep them separate.
Another practical signal: if package version numbers need to advance independently (an SDK package carries a strict public semver commitment, while the internal app's version number means nothing), forcing them into one workspace also complicates the automatic version bumps Conventional Commits generates. It's healthier to keep such packages in separate repos first, and only move a subset into a workspace once it genuinely starts changing together.
Typical monorepo candidates
- Mobile app + backend SDK package: a model change in the SDK should immediately produce a build error in the app; in a separate repo that feedback arrives days later.
- Main app + design system package: if they're being developed at the same time, they need to stay in sync with a single
pub get. - Multiple Flutter apps + a shared core layer: if you're applying the layered architecture I described in flutter-clean-architecture inside a single app, you don't need a monorepo; once you're sharing those same layers across multiple apps, Melos comes into play.
Melos Setup and Configuration Anatomy
Melos describes itself as a "monorepo management tool": a tool for managing Dart and Flutter repos that contain multiple packages, which also supports automatic versioning via Conventional Commits. Installation is one line:
1# Activate Melos as a global pub package2dart pub global activate melosMelos isn't a dependency resolution engine itself — it's built on top of Dart's own pub workspaces mechanism. So your team and CI use the same Melos version, also add it as a dependency in the root pubspec.yaml with dart pub add melos --dev; the pubspec's version then takes precedence over the global activation. The first setup step is defining a workspace: list in your root pubspec.yaml:
1name: my_project2environment:3 sdk: ^3.9.04publish_to: none5workspace:6 - packages/helper7 - packages/client_package8 - packages/server_packageA single field is added to each sub-package's own pubspec.yaml; this field "registers" the package to the workspace:
1name: client_package2resolution: workspace3environment:4 sdk: ^3.9.0Note: this isn't a separate melos.yaml file — Melos's script and versioning settings also live directly under a melos: key inside the root pubspec.yaml. So it's one file, one source: both the workspace list and Melos-specific commands live in the same pubspec.yaml.
Writing out package paths one by one can get tedious in large repos; as of Dart 3.11.0 (February 11, 2026), the workspace definition supports glob patterns, meaning you can auto-include every package in a folder with an expression like packages/* — as of March 2026 this feature was stable.
Using a package you've registered to the workspace looks no different from adding a normal pub dependency — it's enough to write helper: ^2.3.0 in client_package's pubspec.yaml (the local helper version also needs to satisfy this constraint), and you don't need to define a path: dependency on top of it:
1// client_package/lib/main.dart2import 'package:helper/helper.dart';3 4void main() {5 final result = HelperUtils.formatCurrency(1990);6 print(result);7}When bootstrap runs, this import resolves to the local source of the helper package inside the workspace — a change you make in helper becomes visible in client_package instantly, without waiting for pub get or a publish. This is where the monorepo's real benefit becomes concrete: shared code behaves like a real-time dependency.
The first command after setup should be bootstrap:
1melos bootstrapThis command does three things at once: it installs each package's dependencies (by running pub get internally), synchronizes shared dependencies across packages, and triggers bootstrap lifecycle scripts if any are defined.
Drawing Package Boundaries: Feature, Core, Design System
Melos's official documentation is extremely simple about the directory structure it recommends: a split at the root between apps/ (the folder holding your apps) and packages/ (where shared packages live).
1my_project2├── apps3│ ├── apps_14│ └── apps_25├── packages6│ ├── package_17│ └── package_2The "feature package / core package / design-system package" split beyond this isn't an official Melos or Flutter convention — it's a split I use in my own projects. I generally prefer splitting into three categories:
- core: network layer, model classes, common utility functions — contains no UI code at all.
- design-system: theme, color tokens, shared widgets — the common visual language of all apps, like the
apps_1/apps_2mentioned in the previous step. - feature: a package covering a specific workflow (login, checkout, profile) end-to-end, containing both UI and logic.
The only advantage of this three-way split is that it clarifies dependency direction: feature packages can depend on core and design-system, but core should never depend on a feature package. No Melos feature enforces this; the discipline is preserved through code review.
Dependency Alignment and Version Conflicts
By definition, pub workspaces means all packages use a single shared dependency resolution — and Dart doesn't allow multiple versions of the same package to be resolved at once. This means that the moment you move to a workspace, all your packages have to converge on the same http or dio version; a situation where package A uses 5.x and package B uses 4.x is no longer possible.
There's a beneficial side to this too: if packages inside the workspace depend on each other, regardless of the declared source (pub.dev, git, path), it automatically resolves to the local version. So even if client_package depends on helper via pub.dev as ^2.3.0, in the same workspace it's not that pub.dev version that runs but the local code right next to it — the core mechanism for reflecting a change instantly across all dependent packages, without publishing.
In practice, conflict resolution works like this:
Situation | Result |
|---|---|
Two packages want a compatible version range of the same dependency | A single resolution is found without issues |
Two packages want an incompatible version range of the same dependency | pub get fails, manual alignment is required |
A package references another package in the workspace via a pub.dev version | It's automatically routed to the local workspace version |
In practice, the easiest way to manage this is to consolidate the version constraints of frequently used third-party dependencies (HTTP client, state management, test helpers) in one place. Some teams do this via a root-level "shared dependencies" package: every feature package depends on it, and actual version numbers live only there. Neither Melos nor Dart enforces this pattern, but it's a practical habit that reduces version conflicts.
Scripts: Bootstrap, Analyze, Test, Format
When you want to define your own commands, you add a scripts: block under the melos: key in the root pubspec.yaml. The concrete example in the official documentation is a generate script that triggers code generation in packages that depend on build_runner:
1melos:2 scripts:3 generate:4 run: melos exec -c 1 --depends-on build_runner -- dart run build_runner buildTo run this script:
1melos generateThe official example only shows generate concretely; scripts like analyze, test, and format aren't provided ready-made — you define them yourself with the same scripts: structure. Three more examples with the same pattern:
1melos:2 scripts:3 analyze:4 run: melos exec -- dart analyze5 test:6 run: melos exec -- dart test7 format:8 run: melos exec -- dart format --set-exit-if-changed .Each is triggered as melos analyze, melos test, melos format. melos exec is the core mechanism that runs a defined script inside each package's own directory in the workspace; by default up to 5 packages run concurrently, and you can serialize this with -c 1.
Automatic Versioning and Changelog via Conventional Commits
The second sentence of Melos's own definition points exactly to this: it supports automatic versioning via Conventional Commits. So when you write commit messages with standard prefixes like feat:, fix:, chore:, Melos can read this history and figure out on its own which version bump (patch/minor/major) each package needs, and generate the changelog accordingly.
The real value shows up in a monorepo: a single commit can affect multiple packages, and manually tracking each package's version number quickly becomes unsustainable. The Conventional Commits + Melos combination automates this — where commit-message discipline pays off.
Testing Only the Changed Package in CI
As a monorepo grows, the biggest CI cost is running the tests of every package in the workspace from scratch on every push — and this waste grows as the repo grows. The goal here is clear: if a commit only changed packages/design_system, there's no need for CI to also re-run packages/payments's tests from scratch.
The direct fix is Melos's own --diff flag: it filters which packages changed relative to a commit or commit range, then runs the script only on those packages.
1melos exec --diff=origin/main...HEAD -- dart test--diff=<commit hash> compares against a single commit, while --diff=<start>...<end> filters based on the range between two commits. You can also detect changed directories with a manually written git diff and feed that into your own script — but treat this as a complement to --diff, not a replacement (for example, if you have custom mapping logic that doesn't fit Melos's filter syntax).
A simple mental model: the pipeline first extracts changed file paths with git diff --name-only origin/main...HEAD, maps those paths to package roots (packages/<name>/), then triggers the test script only for the matching packages. If the changed file is in a core package and you also want to run tests for feature packages depending on it, Melos already provides this: --include-dependents expands the filtered package list to packages that depend on them (transitive dependents) — conversely, --include-dependencies also pulls in a package's own dependencies.
1melos exec --diff=origin/main...HEAD --include-dependents -- dart testYou can add this filtering as an extra step to the GitHub Actions pipeline described in flutter-ci-cd-github-actions-fastlane.
Relationship to Pub Workspaces
As you've seen in every step so far, Melos isn't inventing a dependency resolution system from scratch — it adds an orchestration layer on top of Dart's own official pub workspaces feature. Pub workspaces itself arrived with Dart 3.6.0 (December 11, 2024), and since then every workspace package's environment.sdk constraint has needed to be at least ^3.6.0. Melos bases the first step of its setup on this by pointing directly to the "Pub Workspaces" guide.
This distinction matters in practice, because some teams ask "we already have pub workspaces, why do we need Melos?" The answer: if all you need is shared dependency resolution, you really can get by without Melos — manually managing the workspace: list and resolution: workspace fields and running dart pub get from the root is enough. Where Melos comes in is when you want to run repeating commands (test, format, analyze) across multiple packages in one go, and automate version/changelog management.
So what does Melos add over bare pub workspaces? Here's how I'd summarize the difference:
Feature | Pub workspaces alone (Dart) | With Melos |
|---|---|---|
Single resolution / shared dependency | Present (built-in) | Uses it as-is, doesn't add to it |
Running commands across packages ( exec) | Absent | Present via melos exec |
Automatic version/changelog from Conventional Commits | Absent | Present |
Bootstrap lifecycle scripts | Absent | Present |
Package discovery via glob ( packages/*) | Present (Dart 3.11.0+) | Uses it as-is |
So defining the workspace list via glob (packages/*) is entirely Dart's own feature — Melos inherits it, it doesn't reinvent it. Melos's real contribution is building a command and versioning layer on top of this shared resolution.
Migration Cost and Rollback
The most concrete friction point when moving existing packages from separate repos into a single workspace is "stray files": old pubspec.lock and .dart_tool/package_config.json files left in the directories between the root and each workspace package are no longer valid once you've moved, and pub get deletes them. In practice, the first melos bootstrap run after migration deletes and regenerates these files (they're dependency resolution artifacts, not a build cache) — so it isn't a surprise, I'd plan migration night as a maintenance window.
There's no official guide on rolling back (breaking apart the workspace, splitting packages back into separate repos); it's entirely manual reverse-engineering — remove resolution: workspace from every package and regenerate its own independent pubspec.lock. Treat the migration decision as permanent, not as "I'll try it for a few weeks and go back."
Don't underestimate the human side either: if part of your team is used to separate repos, the first weeks may bring confusion like "why did my change break another package's tests" — actually an expected, even intended, outcome of single resolution. A short internal training session in the first sprint after migration (when to run bootstrap, how scripts trigger, how version bumps work) leaves far fewer question marks later.
GOLDEN TIP
The most valuable insight in this article
This tip holds the article's most important takeaway.
Easter Egg
You found a hidden gem!
There's a hidden detail in this section. Want to uncover it?
Reader Reward
The steps skipped when moving to Melos usually show up not in the first weeks but the third month — when a new team member joins, or CI suddenly slows down. Go through this checklist before and after migration; each item maps to a point covered in this post.
FAQ
How do you set up a monorepo in Flutter?
You create a pubspec.yaml at the root and list your sub-package paths under workspace:, add resolution: workspace to each sub-package's own pubspec.yaml, and then run melos bootstrap. These three steps are the foundation of the plain Dart pub workspaces mechanism; Melos adds a scripting and versioning layer on top.
What is Melos for?
Melos is a tool designed to manage Dart/Flutter repos containing multiple packages: it supports running commands across packages (melos exec), defining shared scripts (now under the melos: key in the root pubspec.yaml instead of a separate file), and generating automatic versioning/changelogs via Conventional Commits.
How do you manage multiple Flutter apps in the same repo?
The directory structure Melos recommends is to keep each app as a separate subdirectory under an apps/ folder, and shared packages as separate subdirectories under a packages/ folder. Each app accesses the shared packages under packages/ in its own pubspec.yaml via workspace resolution instead of a path dependency; the bootstrap command keeps all apps and packages in sync at once.
What's the difference between pub workspaces and Melos?
Pub workspaces is a built-in feature of the Dart SDK itself (since Dart 3.6.0) and only provides shared dependency resolution. Melos builds an additional orchestration layer on top of this foundation: running commands across packages, bootstrap lifecycle scripts, and automatic versioning based on Conventional Commits — none of which exist in bare pub workspaces.
Update (September 2026)
This post was written on March 17, 2026, when Melos 7.4.1 and Flutter 3.41.4 were current. A few concrete things have changed in the ecosystem since then:
- Flutter 3.47 (August 12, 2026): in 3.47 the Material/Cupertino libraries continue to sit in the core SDK for now; alongside that,
material_uiandcupertino_uiwere published on pub.dev as independent 1.0 packages (opt-in), while the official deprecation of the core copies is planned for the November release (for details see flutter-3-47-material-cupertino-paket-gecisi). In a Melos/pub-workspaces monorepo, since all packages share a single rootpubspec.lock, different apps putting different version constraints on these packages may now require a separate alignment decision — this isn't a confirmed case, it's an expected consequence of single shared resolution. Pinning a single version range in the rootpubspec.yamlis a reasonable precaution. - Dart 3.11 (February 11, 2026) and Dart 3.12 (May 2026): glob-based workspace member discovery (
packages/*) arrived; Dart 3.12's release notes also documented that pub workspaces can be nested — this isn't a new feature, the nesting example still only requires the^3.6.0version constraint. Nesting wasn't yet documented on dart.dev as of March 2026; it was documented with Dart 3.12. - Melos's changelog moved forward: as of September 2026, the current version on pub.dev is 8.9.0 (September 21, 2026); only one major version bump (7 to 8) has happened since 7.4.1. The
changedcommand was added in 8.7.0, and thecherry-pickcommand plus the--post-filteroption were added in 8.9.0.
Conclusion
Melos is a thin but effective orchestration layer built on top of Dart's pub workspaces feature: a single pubspec.yaml, a single dependency resolution, and running commands across packages with melos exec. Setup is just three steps — the workspace list, resolution: workspace, melos bootstrap — but the real value shows up once you define the scripts (analyze, test, format) and automate versioning with Conventional Commits.
You can find how to set up a layered architecture inside a single app in flutter-clean-architecture; using these two approaches together lets you build an architecture that's clean within each package and aligned across packages. You'll find how to integrate these packages into a CI pipeline in flutter-ci-cd-github-actions-fastlane. If you want to see the equivalent of the same multi-module logic on the native iOS side, check out modular-architecture-spm and spm-advanced-modular — the tools differ but the problem is the same: managing shared code from a single place with a consistent versioning discipline. When splitting your state management layer into packages, the patterns in flutter-state-management-riverpod will also come in directly useful.
Sources
- Melos — Getting Started — the official source for setup, workspace definition, bootstrap, and script anatomy.
- Melos package (pub.dev) — the official package listing and version history.
- Melos Changelog — version-by-version change records.
- Dart — Pub Workspaces — the official documentation of the pub workspaces mechanism, the drawbacks of single-repo-multi-package, and the minimum SDK constraint.
- Dart SDK Changelog — the primary source for the Dart 3.6.0 and 3.11.0 release dates.
- Flutter — What's new in Flutter 3.47 — the announcement of the opt-in standalone material_ui/cupertino_ui 1.0 packages (source for the Update section).
- Flutter release notes — the general 3.47 release notes (does not cover the material_ui split).
Tags
iOS Development News
Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.
We respect your privacy. You can unsubscribe at any time.

