# What You’ll Learn#
This guide explains how to scale Flutter using modularization in a monorepo: when it’s worth the complexity, how to structure shared packages, and how to keep package boundaries clean over time.
You’ll set up a Flutter monorepo with Melos, align dependencies across packages, and build a CI and testing strategy that stays fast even as your codebase grows.
For app architecture context (feature-first, Clean Architecture, and practical tradeoffs), read Flutter app architecture: Clean Architecture + feature-first.
# When Modularization Is Worth It (and When It Isn’t)#
Modularization is not a default best practice. It’s a scaling tool, and it introduces overhead in tooling, CI, dependency management, and boundary maintenance.
Modularize when you hit these signals#
| Signal | What you feel day-to-day | Typical threshold |
|---|---|---|
| Team growth | Merge conflicts, unclear ownership, code review slows down | 4+ engineers touching the same areas weekly |
| Build and test time pain | Full test suite takes too long, local rebuilds feel sluggish | Unit tests 10+ minutes, frequent full rebuilds |
| Multiple apps | You need shared design system, auth, analytics, domain logic | 2+ apps or a white-label setup |
| High coupling | Features import each other, refactors break unrelated areas | Frequent regressions outside changed feature |
| Release risk | Hard to reason about changes, missing isolation | Hotfixes take longer than planned |
🎯 Key Takeaway: Modularize to reduce coupling and speed feedback loops, not to make the folder tree look clean.
Don’t modularize yet if this is you#
| Scenario | Better approach |
|---|---|
| Single app, 1 to 3 devs | Keep one package, use feature-first modules inside lib/ |
| Fast-changing product discovery | Optimize for iteration, revisit modularization later |
| No stable boundaries | Define features and ownership first, then split packages |
| You lack CI discipline | Fix tests and automated checks before adding repo complexity |
A useful rule: if you cannot clearly describe what goes into each package in one sentence, you’re not ready to split.
# Monorepo Foundations: Recommended Structure#
A Flutter monorepo should encode architecture decisions in its filesystem. The goal is predictable ownership, minimal cross-package coupling, and safe reuse.
A practical package taxonomy#
Use a small number of package types and keep them consistent:
| Package type | Purpose | Can depend on | Should not depend on |
|---|---|---|---|
apps/* | Deployable Flutter apps | features/*, shared/* | Other apps |
features/* | Feature modules, UI plus feature-specific logic | shared/* | Other features (prefer shared abstractions) |
shared/* | Cross-cutting libraries | Other shared/* (carefully) | features/* or apps/* |
tools/* | CI scripts, generators, dev tooling | Anything (dev only) | Runtime app code |
Example folder layout#
| Path | Example | Why it exists |
|---|---|---|
apps/customer_app | main consumer app | Releases to stores |
apps/admin_app | internal admin app | Shares auth and design system |
features/checkout | checkout UI and flows | Owned by a feature team |
shared/design_system | components, tokens | Prevents UI duplication |
shared/core | logging, error types, env | Keeps app glue minimal |
shared/api_client | HTTP client and interceptors | Avoids copy-paste API setup |
shared/domain_models | stable models used across features | Avoids circular deps |
⚠️ Warning: A “shared” package without a strict purpose becomes a dumping ground. If a file doesn’t clearly belong, create a new focused package or keep it in the feature.
# Setting Up Melos for a Flutter Monorepo#
Melos is the most common workspace tool for Dart and Flutter monorepos. It automates bootstrapping local path dependencies, running scripts across packages, and aligning dependencies.
Step 1: Create melos.yaml#
Place it at repo root.
name: samioda_flutter_workspace
packages:
- apps/**
- features/**
- shared/**
command:
bootstrap:
usePubspecOverrides: true
scripts:
analyze:
run: melos exec --fail-fast -- dart analyze .
description: Run static analysis in all packages
test:
run: melos exec --fail-fast -- dart test
description: Run unit tests in all packages that have them
flutter_test:
run: melos exec --fail-fast -- flutter test
description: Run Flutter tests where applicable
format:
run: melos exec -- dart format --output=none --set-exit-if-changed .
description: Enforce formatting across packagesStep 2: Bootstrap#
melos bootstrapThis wires local path dependencies, so packages can depend on each other without publishing to pub.dev.
💡 Tip: Commit the
pubspec_overrides.yamlfiles if your team benefits from reproducible local setup, but ensure CI runsmelos bootstrapso overrides stay consistent.
Step 3: Align SDK and Flutter constraints#
In every package pubspec.yaml, keep SDK constraints identical.
| Constraint | Recommendation | Why |
|---|---|---|
| Dart SDK | One shared range, for example >=3.4.0 <4.0.0 | Avoid analyzer and build differences |
| Flutter SDK | Same Flutter channel and version pinned in CI | Prevent “works on my machine” |
To enforce this, add a CI check that fails when constraints drift. Treat it like a dependency lock for monorepos.
# Designing Shared Packages That Stay Useful#
Shared packages are where monorepos either shine or collapse into spaghetti. The trick is making shared packages small, stable, and opinionated.
Shared package patterns that scale#
| Pattern | Example package | What it contains | What it avoids |
|---|---|---|---|
| Core utilities | shared/core | error types, logging, env config | UI, feature logic |
| API layer | shared/api_client | Dio setup, auth interceptors, retry | Feature endpoints |
| Design system | shared/design_system | buttons, typography, spacing tokens | Feature screens |
| Observability | shared/analytics | event interface, adapters | Business logic decisions |
| Domain primitives | shared/domain_models | Money, Address, IDs | API DTOs tied to backend changes |
A strong practice is to treat shared packages as products with versioned APIs, even if you never publish them.
Public API discipline with barrel files#
Expose a narrow public surface with explicit exports.
// shared/core/lib/core.dart
export 'src/logging/logger.dart';
export 'src/errors/app_exception.dart';
export 'src/env/app_env.dart';Consumers import package:core/core.dart, not deep paths. This reduces refactor cost and makes boundary enforcement easier.
Preventing circular dependencies early#
Circular dependencies are common when teams create “helpers” that depend on feature code. Keep dependencies flowing in one direction.
| Layer | Allowed direction |
|---|---|
| Apps | down to features and shared |
| Features | down to shared |
| Shared | only to shared utilities, never to features or apps |
If a shared package needs feature-specific behavior, use an interface in shared and implement it in the feature or app.
# Feature Packages: What Goes Where#
Feature packages should be cohesive units that a team can own, test, and release as part of an app without touching unrelated code.
A feature package template#
| Folder | Contents | Practical rule |
|---|---|---|
lib/src/ui | screens, widgets | UI depends on feature state only |
lib/src/state | BLoC, Riverpod, controllers | No direct HTTP, call use cases |
lib/src/domain | entities, use cases | Pure Dart, no Flutter imports |
lib/src/data | repositories, mappers | Data depends on API client, storage |
This aligns naturally with Clean Architecture and feature-first organization. For the bigger picture, see our Clean Architecture + feature-first guide.
Dependency injection across packages#
Avoid a global service locator in shared packages. Let the app compose dependencies and pass them down.
A simple composition root in the app package keeps boundaries clean.
// apps/customer_app/lib/main.dart
import 'package:flutter/material.dart';
import 'package:api_client/api_client.dart';
import 'package:checkout/checkout.dart';
void main() {
final client = ApiClient(baseUrl: 'https://api.example.com');
final checkoutDeps = CheckoutDependencies(apiClient: client);
runApp(CustomerApp(checkout: checkoutDeps));
}Keep dependency objects small and explicit. If a feature needs 12 dependencies, it’s a signal the feature package is doing too much.
# Keeping Boundaries Clean (Without Slowing the Team)#
Boundaries fail when they are “suggestions.” You need lightweight enforcement that catches violations during PRs.
Use lint rules consistently per package#
At minimum, enforce flutter_lints or dart_lints everywhere, then add custom rules gradually.
| Rule category | Why it matters in monorepos |
|---|---|
| Import hygiene | Prevent deep imports and accidental cross-feature coupling |
| Unused dependencies | Stops dependency creep and reduces build time |
| Analyzer strictness | Makes refactors safe across packages |
Run analysis via Melos so it stays consistent.
melos run analyzeEnforce import boundaries with architecture tests#
A practical approach is a small “architecture test” package that scans imports and fails if rules are violated. Even a simple grep-based rule catches most issues.
# tools/check_boundaries.sh
set -e
# Disallow features importing other features directly
if grep -R "package:features_" -n features | grep -v "shared/" ; then
echo "Boundary violation: feature importing another feature"
exit 1
fiKeep the rules minimal and aligned with real architecture. Overly strict rules create workarounds and reduce trust.
ℹ️ Note: If you use code generation heavily, exclude
*.g.dartand build output folders from boundary checks to avoid noisy failures.
Dependency creep control#
In monorepos, the biggest silent cost is dependency duplication and transitive bloat.
| Control | Implementation | Result |
|---|---|---|
| Dependency allow-list | Document what belongs in shared/core vs feature | Fewer “quick adds” |
| Monthly dependency review | Remove unused, consolidate HTTP, JSON, DI libs | Smaller app size, faster builds |
| Unified versions | Same dio, freezed, riverpod versions | Fewer conflicts |
For performance implications, see Flutter performance optimization for 60 FPS. Bigger dependency graphs and excessive rebuild triggers translate into dropped frames and slower startup.
# Dependency Management in a Monorepo#
You need two things: alignment and autonomy. Alignment prevents conflicts, autonomy prevents lockstep changes for everything.
Recommended dependency strategy#
| Dependency type | Where to declare | Why |
|---|---|---|
| Core tooling | Root docs and CI scripts | Consistent across workspace |
| Shared runtime libs | shared/* packages | Centralize stable primitives |
| Feature-specific libs | features/* packages | Avoid bloating the entire workspace |
| Dev dependencies | Per package | Keeps toolchains focused |
When multiple packages need the same library, prefer adding it to the most specific shared package that owns that responsibility, not to every feature.
Version pinning#
Monorepos tend to “float” versions until something breaks. Use explicit version pins for critical libraries and upgrades via scheduled maintenance.
A pragmatic cadence is one dependency upgrade day every 2 to 4 weeks, plus security updates immediately.
# Testing Strategy Across Packages#
Modularization should reduce test scope and speed up feedback. If tests still require running the entire app, modularization is not delivering value.
What to test where#
| Package type | Primary tests | Secondary tests |
|---|---|---|
| Shared pure Dart | Unit tests | Contract tests for adapters |
| Shared Flutter UI | Golden tests, widget tests | Accessibility checks |
| Feature packages | Unit tests for domain, widget tests for UI | Integration tests in app |
| App packages | Integration tests, smoke tests | End-to-end flows per release |
Make features testable without the app#
Design feature packages so they can be rendered in isolation with injected dependencies.
// features/checkout/test/checkout_widget_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:checkout/checkout.dart';
void main() {
testWidgets('shows empty cart state', (tester) async {
final deps = CheckoutDependencies.fake(emptyCart: true);
await tester.pumpWidget(CheckoutTestApp(deps: deps));
expect(find.text('Your cart is empty'), findsOneWidget);
});
}This is how you keep CI fast: features can be verified without building the full app.
Avoid flaky tests in a monorepo#
Flaky tests waste more time than slow tests. Treat flakiness as a defect.
| Common flake source | Fix |
|---|---|
| Timers and animations | Use fake async or pump with deterministic durations |
| Network calls | Use fakes, not real HTTP |
| Golden tests on different machines | Pin Flutter version, use consistent fonts and rendering setup |
# CI for Flutter Monorepos: Fast PRs, Reliable Releases#
CI needs to be selective, cached, and consistent. The biggest win is skipping work when a change only affects one feature.
For CI/CD platform specifics, see Flutter CI/CD with GitHub Actions, Codemagic, and Fastlane.
CI pipeline stages that work well#
| Stage | Runs on | Goal |
|---|---|---|
| Lint and format | every PR | catch cheap issues early |
| Unit tests per package | every PR | fast correctness signal |
| Build apps | when app or shared packages change | verify compilation and assets |
| Integration tests | nightly and before release | verify critical flows |
| Release | tagged commits | deterministic store builds |
Path-based job selection#
Use path filters so a docs change does not trigger app builds. Also, if only one feature changes, run tests for that feature plus any shared packages it depends on.
Even without advanced dependency graph tooling, a simple mapping provides large gains.
| Changed path | Minimum jobs |
|---|---|
shared/core/** | analyze and test all packages, build apps |
features/checkout/** | analyze and test checkout, build apps that use it |
apps/customer_app/** | analyze and test customer app, integration tests optionally |
💡 Tip: Start with conservative selection, then optimize. A monorepo CI that misses failures is worse than a slower CI.
Caching essentials#
Cache what actually takes time:
| Cache | What it speeds up | Notes |
|---|---|---|
| Pub cache | dependency fetch | Works across jobs if keyed correctly |
| Build outputs | incremental builds | Often tied to Flutter version and OS |
| CocoaPods | iOS builds | Cache pods folder cautiously |
Pin Flutter version in CI. Small differences in Flutter or Dart patch versions can break golden tests and create hard-to-debug inconsistencies.
# Common Pitfalls and How to Avoid Them#
- 1
Creating too many packages too early
Start with a few high-value splits: design system, API client, and one or two large features. - 2
Letting features depend on each other
If two features need shared logic, extract it into a focused shared package with a stable API. - 3
Shared package becomes a “utils” bin
If the package name is vague, it will become vague. Prefer specific names likeapi_client,observability, ordesign_system. - 4
Monorepo without boundary enforcement
Add at least one automated boundary check in CI and rundart analyzeeverywhere on PRs. - 5
CI always runs everything
Monorepos fail when feedback loops become slow. Introduce path filters and package-scoped tests early.
# Key Takeaways#
- Modularize when team size, build times, or multi-app reuse create real pain; don’t split just for structure.
- Use a clear taxonomy:
apps/*,features/*,shared/*, and keep dependencies flowing one way. - Keep shared packages small and stable with explicit public APIs and no dependencies on features or apps.
- Enforce boundaries via consistent linting and simple automated checks that block cross-feature imports.
- Optimize CI with path-based job selection, aggressive caching, and package-level tests to keep PR feedback fast.
# Conclusion#
A Flutter monorepo with Melos pays off when it reduces coupling and speeds up delivery. The core work is not the tooling setup, but designing packages with clear responsibilities, enforcing boundaries automatically, and building a CI pipeline that runs only what changed.
If you want help implementing Flutter monorepo Melos modularization in a real product, Samioda can audit your current architecture, propose a package split plan, and set up CI and testing so your team can ship faster with fewer regressions.
FAQ
More in Mobile Development
All →Flutter vs Native iOS/Android in 2026: Cost, Performance, and Time-to-Market Tradeoffs
A practical, numbers-driven comparison of Flutter vs native iOS and Android for 2026 — including cost model, performance reality, maintenance impact, and a decision framework for MVPs, high-performance UI, heavy platform APIs, and regulated apps.
Flutter Deep Linking Guide for 2026: Universal Links, Android App Links, and Reliable In-App Routing
A practical, production-ready guide to Flutter deep linking: Universal Links, Android App Links, go_router route handling, deferred deep links, attribution basics, and a troubleshooting checklist.
Flutter CI/CD in 2026: GitHub Actions vs Codemagic vs Fastlane (With a Production Pipeline Blueprint)
A practical 2026 guide to Flutter CI/CD: compare GitHub Actions, Codemagic, and Fastlane, then implement a production-ready pipeline with flavors, signing, build numbers, tests, code generation, caching, and store deployments.
Need help with your project?
We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.
Related Articles
Flutter App Architecture That Scales: Clean Architecture vs Feature-First (With Real Folder Structures)
A practical guide to Flutter app architecture in 2026: compare Clean Architecture and Feature-First, see real folder structures, dependency boundaries, and testing strategies, and choose the right approach for your team and release cadence.
Flutter CI/CD in 2026: GitHub Actions vs Codemagic vs Fastlane (With a Production Pipeline Blueprint)
A practical 2026 guide to Flutter CI/CD: compare GitHub Actions, Codemagic, and Fastlane, then implement a production-ready pipeline with flavors, signing, build numbers, tests, code generation, caching, and store deployments.
Flutter vs Native iOS/Android in 2026: Cost, Performance, and Time-to-Market Tradeoffs
A practical, numbers-driven comparison of Flutter vs native iOS and Android for 2026 — including cost model, performance reality, maintenance impact, and a decision framework for MVPs, high-performance UI, heavy platform APIs, and regulated apps.