<FlutterSolution/> flutter · dart · clean architecture

Flutter Interview Prep # 20 Modularizing a Flutter App with Melos: From Monolith to Monorepo

Modularizing a Flutter App with Melos: From Monolith to Monorepo

Stop rebuilding your entire app to test one widget. Learn how to draw hard boundaries and scale your codebase when a single `lib/` folder breaks down.

01. The Breaking Point of a Monolith

You don't modularize an app because an architecture diagram told you to. You modularize because the physical boundaries of a single codebase have started to cost your team money and time.

As a single Flutter app grows—let's use a Food Delivery app as our example—the `lib/` folder becomes a free-for-all. You will start noticing three specific symptoms:

  1. Glacial CI Pipelines: A developer changes a color on the "Profile" screen. The CI server triggers a rebuild and retest of the entire application, including the complex Ordering and Payment flows. Your 2-minute pipeline now takes 15 minutes.
  2. Merge Conflict Hell: Two teams are working on different features (Auth and Order Tracking), but because they share top-level routing, DI, and state files, they constantly trample on each other during merges.
  3. Accidental Spaghettification: A junior developer working on the Checkout screen needs a user's address, so they blindly import a highly specific `LocationTrackerService` from deep inside the Order Tracking feature. A brittle, invisible dependency is born.

Frame modularization as the direct answer to these symptoms. The goal isn't just organized folders; the goal is enforced boundaries and isolated execution.

02. Enter Melos: Automating the Monorepo

Dart's package system allows you to split code into local packages. But manually running `flutter pub get` or `flutter test` across 15 different folders every time you switch branches is miserable.

This is where Melos comes in. Melos is the industry-standard tool by Invertase for managing a Flutter/Dart monorepo (a single Git repository containing multiple independent packages).

What Melos actually automates:

  • Bootstrapping: It links all local dependencies together instantly, so your `app` package can depend on your `feature_auth` package without needing to publish it to pub.dev.
  • Command Execution: It allows you to run `melos run test` from the root, which intelligently executes tests in every package concurrently.
  • Selective CI: Melos knows exactly which packages changed in a PR, allowing you to only run tests for the affected code.

03. The Standard Package Architecture

How do you divide a Food Delivery app? The standard monorepo pattern splits code into three distinct tiers:

Monorepo Dependency Flow (Hover/Tap to trace)
food_app_shell Routing & DI Wiring feature_auth Login, Signup feature_order Cart, Checkout feature_track Map, ETA core_shared Network, Design System

Here is what that looks like physically in your repository:

Directory Structure
food_monorepo/
├── melos.yaml                  # Monorepo configuration
├── apps/
│   └── food_delivery_app/      # App Shell (pubspec depends on features)
└── packages/
    ├── core/                   # API client, Typography, Colors
    └── features/
        ├── auth/               # Depends on core
        │   ├── lib/
        │   │   ├── auth.dart   # PUBLIC API (exports ONLY what others need)
        │   │   └── src/        # PRIVATE (Login screen, Auth BLoC, internal repo)
        │   └── pubspec.yaml
        ├── ordering/           # Depends on core
        └── tracking/           # Depends on core

04. Why This Actually Matters at Scale

During an interview, don't just say "it keeps the code clean." Detail the mechanical benefits of this structure:

  • Enforced Boundaries via Public APIs: In Dart, anything under a package's lib/src/ folder is considered private to that package. If `feature_ordering` needs to know if a user is logged in, it CANNOT import `auth/lib/src/auth_bloc.dart`. Dart's compiler will literally throw an error. It can only import what `auth/lib/auth.dart` explicitly exports (like an `AuthState` enum).
  • Independent Testing: A developer working on the `tracking` package can run their widget tests locally in milliseconds. They don't need to spin up the Auth state or mock the Ordering cart, because their package doesn't know those things exist.
  • Drastically Faster CI:
Without Melos (Monolith CI on a 1-line change) 12:00 min
With Melos (Change in `feature_auth` only) 02:30 min

Melos filters tests via `melos run test --check-workspace`. If only Auth changed, it only runs tests for Auth and the App Shell (which depends on Auth). Core and Ordering are skipped entirely.

05. Scoping Dependency Injection Across Modules

A major real-world hurdle candidates stumble on is Dependency Injection (DI). If `feature_ordering` is an isolated package, how does it register its BLoCs and UseCases into the app's `get_it` instance?

The solution is Module-Scoped DI. A common pairing is `get_it` + `injectable`. Instead of the App Shell registering every internal class of every feature, each feature package owns its own DI registration function.

packages/features/ordering/lib/di.dart
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';

// This function is exported in the public API (ordering.dart)
@InjectableInit(
  initializerName: 'initOrderingModule',
)
void setupOrderingDI(GetIt getIt) => initOrderingModule(getIt);

Then, during startup, the App Shell simply loops through the public initializers of the features it consumes:

apps/food_delivery_app/lib/main.dart
void main() {
  final getIt = GetIt.instance;
  
  // App Shell doesn't know HOW things are registered, just THAT they are.
  setupCoreDI(getIt);
  setupAuthDI(getIt);
  setupOrderingDI(getIt);
  
  runApp(FoodDeliveryApp());
}

06. Honest Take: When NOT to Modularize

😌 Honest Take: The Pre-Modularization Trap

Modularization comes with a heavy ceremony cost. You now have multiple `pubspec.yaml` files to keep in sync. Setting up CI takes longer. Navigating code requires jumping between IDE workspaces.

If you are building an early-stage MVP where feature boundaries are still shifting rapidly (e.g., "Wait, is Wallet part of Ordering or Auth?"), stay a monolith. The speed of iteration matters more than CI times for a team of two.

A senior candidate knows that modularization is a solution to a specific scaling problem. Applying it before you have the problem is over-engineering.

07. How to Actually Start (The Incremental Approach)

A favorite interviewer follow-up is: "Okay, our current app is a 100,000-line monolith. Walk me through how you'd migrate it without stopping product development."

Never suggest a big-bang rewrite. You migrate incrementally, from the bottom up, then the edges in:

1
Extract the Core UI
Create a `core_ui` package. Move your app's custom buttons, typography, and colors here. It has zero business logic dependencies, making it safe to extract first.
2
Extract a "Leaf" Feature
Pick a feature that doesn't depend on much else—like the "Settings" or "Profile" screen. Move it into `feature_settings`. Wire it back to the monolith shell.
3
Setup Melos & CI
Introduce Melos to link the new `core_ui` and `feature_settings` to the legacy monolith. Adjust CI to run tests selectively. Repeat step 2 for the rest of the app.
🧠 Interviewer's Follow-Up

Expect these pushbacks when discussing monorepos:

  • "How do you handle routing if features don't know about each other?"
    Answer: "Feature packages never import each other's screens. They expose route builders. The App Shell defines the `GoRouter` configuration and maps deep links to the respective feature route builders."
  • "What if `feature_ordering` genuinely needs the logged-in user's ID from Auth?"
    Answer: "The Auth package exports a highly focused interface in its public API, like `IAuthFacade`, which Core defines or Auth exposes. Ordering depends only on that interface, never on the UI or BLoCs of Auth."

08. Senior-Level Sample Answer

When asked to describe how and why you'd modularize a Flutter app, rely on concrete symptoms and mechanisms. Use this template:

"As our codebase grew, we started experiencing glacial CI times and constant merge conflicts because everyone was touching the same top-level files. To fix this, we transitioned to a Melos monorepo. We extracted our code into three tiers: a shared `core` package, independent feature packages like `auth` and `ordering`, and an App Shell that wires everything together.

This solved the problem mechanically: Dart's package system strictly prevents `ordering` from importing `auth`'s internal UI, enforcing clean boundaries. And because Melos knows the dependency graph, our CI pipeline only rebuilds and tests the specific packages that were altered in a PR, cutting our feedback loops significantly."
✅ Quick Self-Check

Before moving to the next post, answer this out loud: If `feature_auth` needs to use a custom primary button widget, which package should hold that widget, and what is the dependency direction?

(Answer: A `core` or `core_ui` package should hold it. `feature_auth` depends on `core`. `core` never depends on a feature.)

Official Resources

Nachiketa Pandey
// written by

Nachiketa Pandey

Senior Flutter Developer · Cross-Platform & Fintech Applications

Building production Flutter apps at Kotak Neo — clean architecture, BLoC, agentic AI integrations, and fintech-grade security. Sharing everything I learn, one post at a time.

Flutter & Dart Clean Architecture BLoC Firebase FinTech
View Full Portfolio

Comments

Nachiketa
Meet Author// tap to open