Flutter Project Structure: Monorepo vs. Multi-Package vs. Multiple Repositories
The Flutter news you actually need
No spam, ever. Unsubscribe in one click.
As your Flutter application grows from a simple prototype to a complex product supporting multiple platforms and teams, how you organize your code becomes as critical as the code itself. A haphazard structure can turn adding a simple feature into a debugging nightmare, while a well-considered one can streamline development for years.
Let’s break down three common structural approaches: the single-module monorepo, the multi-package monorepo, and the multiple-repository strategy. We’ll explore when each shines and when it might become a burden.
The Straightforward Monorepo (Single Module)
This is the default Flutter project structure created by flutter create. Everything lives in the lib/ folder: widgets, models, services, and screens are all mixed together.
// lib/main.dart
// lib/models/user.dart
// lib/services/api_service.dart
// lib/screens/home_screen.dart
// lib/widgets/custom_button.dart
Pros:
- Simplicity: No build configuration overhead. It’s easy to navigate and understand for small projects or solo developers.
- Refactoring Ease: Changing a model’s field? Your IDE can find all usages instantly because everything is in one place.
- Fast Builds: For small to medium projects, builds are generally quick.
Cons:
- Tight Coupling: It’s very easy for UI code to directly depend on business logic and data layers, creating spaghetti code.
- Scalability Issues: As
lib/balloons to hundreds of files, navigation slows down, and mental overhead increases. - Team Conflicts: With multiple developers, merge conflicts become more frequent in the core
pubspec.yamland other shared files.
Best for: Prototypes, small apps with a single focus, or projects with a very small team.
The Structured Monorepo (Multi-Package)
This approach organizes your codebase into separate, independently versioned Dart packages within a single repository. You use a pubspec.yaml for the main app and one for each internal package (often placed in a packages/ folder).
my_app/
├── pubspec.yaml
├── lib/
│ └── main.dart
└── packages/
├── analytics/
│ ├── lib/
│ └── pubspec.yaml
├── user_repository/
│ ├── lib/
│ └── pubspec.yaml
└── ui_kit/
├── lib/
└── pubspec.yaml
Your main app’s pubspec.yaml references these local packages:
dependencies:
flutter:
sdk: flutter
analytics:
path: packages/analytics
user_repository:
path: packages/user_repository
ui_kit:
path: packages/ui_kit
Pros:
- Enforced Boundaries: Clear separation of concerns. Your
home_screencan’t accidentally import a low-level database class fromuser_repositoryunless it’s explicitly exposed in its public API. - Independent Development: Teams can work on
ui_kitandanalyticsindependently, reducing merge conflicts. - Selective Testing & Building: You can run tests for a single package, speeding up your CI/CD pipeline.
- Potential for Reuse: While within one repo, a well-designed package like
ui_kitcould theoretically be extracted later.
Cons:
- Increased Complexity: Managing multiple
pubspec.yamlfiles and internal dependencies adds overhead. - Refactoring Across Packages: Changing a core interface used by multiple packages requires coordinated updates.
- Build Tools: You may need tools like
melosto orchestrate commands across all packages.
Best for: Medium to large applications, teams of more than 2-3 developers, and projects where clear architectural boundaries (like Clean Architecture or Domain-Driven Design) are important.
The Distributed Approach (Multiple Repositories)
Here, each distinct module or feature lives in its own, standalone Git repository. The main application consumes them as remote dependencies, typically via version tags from a package host like pub.dev or a private Git repository.
dependencies:
flutter:
sdk: flutter
company_ui_kit:
git:
url: https://github.com/yourcompany/ui_kit.git
ref: v1.2.0
payment_processor:
hosted: https://private-pub.dev
version: ^2.0.0
Pros:
- Maximum Isolation & Independence: Teams can develop, version, and release their packages on completely independent schedules.
- True Reusability: Packages are inherently built to be shared across multiple different projects in your organization.
- Fine-Grained Access Control: You can grant repository access only to the teams that need it.
Cons:
- High Overhead: Managing versions, resolving cross-repo dependency conflicts, and synchronizing releases is complex.
- Development Friction: Making a quick bug fix in a dependent package involves switching repos, publishing a version, and then updating the main app. This slows down iteration.
- Tooling Necessity: You’ll likely need a robust CI/CD system and package hosting solution.
Best for: Large organizations with multiple independent product teams, or when creating shared SDKs, design systems, or utility libraries used across many different Flutter and Dart projects.
Common Mistakes & How to Avoid Them
- Over-Engineering Too Early: Don’t start a two-screen app with a multi-package monorepo. Begin with a single module and refactor into packages only when you feel the pain of the monolithic structure (e.g., navigating is hard, teams are stepping on each other’s toes).
- Creating “Silo” Packages: In a multi-package monorepo, avoid creating packages that are so interdependent they must always be updated together. Strive for loose coupling. A good rule of thumb: a package should have a single, clear responsibility.
- Ignoring Dependency Graphs: Use
dart pub depsor themelostool to visualize your dependencies. A tangled, circular dependency graph will cause build and logic problems. Aim for a clear, hierarchical dependency flow (e.g.,domain->data->presentation).
Practical Recommendation
For most real-world Flutter applications that grow beyond a simple MVP, I recommend starting with a single-module monorepo but organizing your lib/ folder with clear subdirectories (e.g., features/, core/, shared/). Once you have 3-4 developers working concurrently or distinct feature domains emerge, refactor into a multi-package monorepo. This balances the need for structure with development speed.
Only consider multiple repositories when you have concrete, current needs for cross-project reuse or completely autonomous teams. The complexity cost is significant, and a well-structured monorepo can serve a surprisingly large project and team effectively.
Ultimately, the best structure is the one that minimizes friction for your team today while leaving the door open for the growth you anticipate tomorrow. Start simple, stay organized, and refactor when the benefits become clear.
This blog is produced with the assistance of AI by a human editor. Learn more
Related Posts
Flutter's Hidden Gem: Building Powerful Linux Desktop Apps with Ease
Flutter is gaining traction as a robust framework for Linux desktop development, offering a smoother experience compared to traditional toolkits like GTK and Qt. This post will explore why Flutter excels for Linux apps, highlight its advantages, and provide practical insights for developers looking to leverage Flutter for their desktop projects.
Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows
Deploying Flutter desktop apps on Windows can be challenging, especially regarding code signing and preventing Windows from blocking installations. This post will guide developers through the process of signing their Flutter desktop applications, exploring alternatives to the Microsoft Store like creating executable installers, and covering the necessary steps to ensure a smooth user experience.
Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows
Developers frequently voice frustrations about Flutter SDK version management and installation processes, particularly on Windows. This post will explore best practices for managing multiple Flutter versions using tools like FVM, discuss integrating Flutter with package managers like Winget for a smoother Windows setup, and offer strategies to ensure a consistent development environment across projects and teams.