Modern Monorepo Management with Pub Workspaces and Melos in Dart
Monorepos store multiple projects in a single repository, providing benefits such as atomic changes, shared dependencies, and unified workflows. However, they can also introduce complexity when it comes to managing various packages and apps. In this article, we’ll explore how to effectively manage a monorepo in the Dart/Flutter ecosystem.
Table of Contents
- Public vs Local Packages
- Managing Dependencies with Pub Workspaces
- Creating Reusable Scripts
- Using Melos for Large Monorepos
- Conclusion (What and When to Use)
Public vs Local Packages
A monorepo typically has two main categories of projects:
- Public packages:
These are released to a public or private registry (for instance, pub.dev). They might include shared packages, reusable frontend widgets, backend libraries, or CLI tools. - Local (internal) packages:
These exist solely for use within the same repository. They commonly provide code like UI components, database abstractions, utility functions, shared features and so on.
Typical Monorepo Structure
A monorepo can be organized in various ways, depending on the number of "main" projects it contains.
When there’s only one main application:
repo/
├── lib/ # Contains the main application
│ └── main.dart
└── packages/ # Contains reusable local packages
├── ui_library/
├── database/
└── core_utils/
When there are multiple applications:
repo/
├── apps/ # Contains multiple Flutter/Dart apps
│ ├── app_1/
│ └── app_2/
└── packages/ # Contains reusable local packages
├── ui_library/
├── database/
└── core_utils/
Grouping Public Packages
Public packages can also be grouped in a single repository to keep related libraries together. For example, the Bloc repository organizes multiple packages under packages/
, such as:
repo/
└── packages/
├── bloc/
├── flutter_bloc/
└── bloc_concurrency/
This approach simplifies making atomic updates across multiple packages, as changes can be applied simultaneously. It also makes management easier since the packages are logically grouped, often sharing a common dependency, like a "bloc." When the "bloc" changes, the dependent packages should be updated as well.
Managing Dependencies with Pub Workspaces
Pub Workspaces, introduced in Dart 3.6 and Flutter 3.27, simplify dependency management in monorepos by organizing multiple projects as a workspace. Here are the key benefits:
- Centralized Dependency Management: All projects in the workspace share a single version of each dependency, ensuring consistency across the monorepo.
dart pub get
downloads dependencies for all packages in workspace. - Unified Upgrades: Running
dart pub upgrade
applies updates across all packages in the workspace. - Improved Dart Analyzer Performance: Issues with monorepos causing degraded analyzer performance have been resolved, making analysis faster and more efficient.
This makes managing monorepos with Dart and Flutter significantly smoother and more efficient.
Setting up a Pub Workspace
Suppose you have the following directory layout:
repo/
├── lib/ # Main application
│ └── main.dart
├── packages/ # Reusable local packages
│ ├── ui_library/
│ ├── database/
│ └── core_utils/
└── pubspec.yaml # Workspace root
Add the resolution: workspace
field to each child package’s pubspec.yaml
:
name: database
description: "Database for the app"
version: 1.0.0
publish_to: none
environment:
sdk: ">=3.6.0 <4.0.0"
flutter: ">=3.27.1 <4.0.0"
# Use shared dependency resolution
resolution: workspace
Add the workspace
field to the root pubspec.yaml
:
name: sizzle_starter
description: A production-ready template for Flutter applications.
publish_to: "none"
version: 0.0.1+1
environment:
sdk: ">=3.6.0 <4.0.0"
flutter: ">=3.27.1 <4.0.0"
workspace:
- packages/ui_library
- packages/database
- packages/core_utils
Once set up, a single dart pub get
at the workspace root resolves dependencies for every package in the repository—no more running the command for each package separately.
Version Mismatch
With a Pub Workspace, only one version of each package is allowed across the entire codebase. If a mismatch occurs, you’ll see an error like:
Because <A> depends on sizzle_lints 2.1.0 and <B>
depends on sizzle_lints ^2.1.3, version solving failed.
By design, this forces you to keep versions in sync, reducing the likelihood of hidden dependency conflicts.
Unified Upgrades
dart pub upgrade
updates all dependencies in the workspace.dart pub upgrade --major-versions
anddart pub upgrade --tighten
also work at the workspace level, updating constraints in all relevantpubspec.yaml
files.
For more details, check out the official Pub Workspaces documentation.
Creating Reusable Scripts
Dependency resolution is just one piece of a bigger puzzle. Many packages require additional commands—for example, running code generation via build_runner or executing scripts to clean, test, or bootstrap projects.
A robust way to handle these tasks is to create reusable scripts. Popular tools for this are Makefile, Taskfile, Shell/Bash scripts. I have included some examples:
Bootstrap Script
This script downloads dependencies for the entire workspace and detects if a package requires build_runner
and triggers code generation only where needed:
#!/bin/bash
# Get workspace dependencies
flutter pub get
# For each package in 'packages/' that uses build_runner, run code generation
for dir in packages/*; do
if [ -f "$dir/pubspec.yaml" ]; then
if grep -q build_runner "$dir/pubspec.yaml"; then
pushd $dir || exit
dart run build_runner build --delete-conflicting-outputs
popd || exit
fi
fi
done
Clean Script
This script cleans up the root project and all child packages, and fixes some local caching issues:
#!/bin/bash
# Clean the main Flutter project
flutter clean
# Clean each package
for dir in packages/*; do
if [ -f "$dir/pubspec.yaml" ]; then
pushd $dir || exit
flutter clean
popd || exit
fi
done
Test and Coverage Script
This script automatically detects packages that contain tests, and then runs them with coverage enabled:
#!/bin/bash
# Exit immediately if a command exits with a non-zero status
set -e
# Find directories with a pubspec.yaml and test folder
find_test_dirs() {
find . -type f -name "pubspec.yaml" -exec dirname {} \; | while read -r dir; do
if [ -d "$dir/test" ]; then
echo "$dir"
fi
done
}
test_dirs=$(find_test_dirs)
if [ -n "$test_dirs" ]; then
flutter test $test_dirs --no-pub --coverage
else
echo "No directories with pubspec.yaml and a test/ folder found."
fi
For small, internal-only monorepos, this script-based approach is simple and effective.
Using Melos for Large Monorepos
When a monorepo grows in size and complexity—especially if it contains multiple publishable packages, elaborate build steps, or complex CI/CD pipelines—managing everything with homemade scripts can become overwhelming. Enter Melos.
Melos used to handle some workspace-like features before Pub Workspaces were introduced, but it remains extremely valuable for:
- Automated Version Management: Supports automatic version bumps and changelog generation, driven by conventional commit messages.
- Flexible Scripting: Easily run commands across all packages, some packages, or filtered groups of packages. Useful when different subsets of packages need specialized tasks.
Getting Started with Melos
Install Melos in dev_dependencies of pubspec.yaml
in the root and activate globally:
dart pub add -d melos
dart pub global activate melos
Create a melos.yaml
file in the repository root (next to pubspec.yaml
):
name: my_workspace
packages:
- packages/*
scripts:
example_script:
description: "Script for demonstration"
run: echo "Hello from Melos!"
hello_flutter:
exec: echo 'Hello from package that uses Flutter $(dirname $PWD)'
packageFilters:
flutter: true
hello_build_runner:
exec: echo 'Hello from package that uses build_runner $(dirname $PWD)'
packageFilters:
build_runner: true
This configuration also includes scripts with the packageFilters
field, allowing targeted execution:
hello_flutter
: Runs only in packages that use Flutter (i.e., packages with Flutter dependencies).hello_build_runner
: Runs only in packages that include thebuild_runner
dependency in theirpubspec.yaml
.
You can run melos
commands as follows:
# Executes the specified example script
melos run example_script
# Automatically handles versioning by examining all relevant commits and adjusting package versions as necessary.
melos version
...
Important: You no longer need to use melos bootstrap
, because dependencies are now resolved through pub workspaces. Melos works seamlessly with them, focusing on higher-level tasks like publishing, versioning, changelogs, and script orchestration across a large set of packages. Find more info on their site.
Conclusion (What and When to Use)
Choosing the right setup for your Dart/Flutter monorepo depends on your project's size, complexity, and goals:
For Small to Medium Projects (with Internal Packages):
- Use
Pub Workspaces
for simple dependency management. - Create lightweight scripts (e.g., Bash, Makefile) for tasks like code generation, testing, and cleaning.
- This approach is straightforward, easy to maintain, and avoids unnecessary complexity.
For Large Projects or Teams:
- Combine
Pub Workspaces
with a tool likeMelos
. - Make use of features like automated versioning, changelog generation, and advanced scripting.
- Ideal for teams needing consistent publishing workflows, robust CI/CD, and coordination across multiple packages or squads.
Hybrid Approach:
- Start with
Pub Workspaces
and basic scripts if your monorepo is still small. - Gradually adopt
Melos
as your needs grow for more structure, automation, and large-scale release management.