Modularization Is a Migration, Not a Decision
Modularization Is a Migration, Not a Decision
There's a conversation I've sat through enough times to have written the script. A mobile org is feeling the build-time pain, the test flakiness, the cross-team friction. Someone proposes modularization. A senior engineer says "we need to modularize the app." Leadership nods. It goes on the Q3 roadmap as a single bullet point. A scoping doc gets written. Two engineers are assigned. The kickoff slide deck says "modularize iOS app" and has three milestones with checkmark icons.
Eighteen months later the team is still working on it, leadership thinks the project is dragging, the product engineers are tired of the constant churn in their day-to-day code, and someone in an offsite says the word "rewrite" out loud.
I have watched this play out in three companies. The mistake is not the engineering. The mistake is treating modularization as a project at all. It is a migration with a multi-year curve, an unavoidable infrastructure component, and org-design implications most leadership underestimates. Treat it like a project and it will rot like a project. Treat it like infrastructure and it will compound.
The fantasy version
The pitch deck version of modularization goes like this. The app is currently a monolith. We carve it into N modules. Build times drop. Teams own their modules. Onboarding gets easier. Q3 deliverable.
That picture leaves out everything that actually takes time. The graph-shape work that determines whether your modules can be built in parallel. The visibility-rules work that prevents the wrong dependencies from creeping back in. The build-system work that has to come along for the ride. The dev-loop work that has to give individual engineers a way to build a single feature in under three minutes. The migration of the existing code, which is not a refactor, it is years of accumulated coupling that has to be slowly unpicked.
Scott Berrevoets' October 2021 writeup of iOS Architecture at Lyft is the cleanest published statement of what this actually requires. Lyft is at roughly 700 modules in the rider app, around 2,000 across the org. The whole point of their module-type system (UI, Flow, Service, Logic, with compiler-enforced dependency validators) is to make the graph shape itself a first-class artifact. The win is not "we have modules." The win is "we have a graph we can reason about." That distinction is the difference between a migration that compounds and a project that fizzles.
Berrevoets' line is the one I keep coming back to: "Doing the right thing is easy, doing the wrong thing is difficult." That is what good modularization is for. The graph enforces the rule, the compiler enforces the graph, and the engineers don't have to remember to do the right thing because the build won't let them do the wrong one.
The four ownership models
I've watched four ownership patterns play out across mobile orgs. Only one of them works.
Pattern A: Everyone owns their module. The most popular pitch and the most reliable failure mode. The premise is that each feature team owns their module's quality, architecture, and dependencies. The reality is that nobody owns the cross-module concerns: the boundary patterns, the visibility rules, the shared abstractions, the build-graph health. Each team optimizes locally. The graph regresses globally. Inside a year you have 200 modules that depend on each other in 200 distinct ways and no single engineer who can describe the system.
Pattern B: The platform team owns everything. Inverse of A. The platform team owns modularization end-to-end and dictates module boundaries, types, and rules to product teams. This works for about six months and then breaks because the platform team becomes the bottleneck for every product change that touches module boundaries. Product engineers grow to resent the layer. Migration cycles take quarters longer than they should.
Pattern C: Shared ownership with no clear edge. "Mobile platform owns the patterns, feature teams own their modules." This sounds right and is what most orgs say they do. In practice nobody owns the edge cases (the cross-cutting features, the shared services, the deprecated modules that nobody wants to touch) and they accumulate as debt until the next reorg.
Pattern D: Platform team owns the graph, feature teams own the modules. The pattern I've seen work. The platform team owns the graph shape: the module types, the visibility rules, the dependency invariants, the build infrastructure. They publish the graph rules and enforce them in CI. Feature teams own the implementation inside the modules they create, but cannot break the graph rules. Cross-cutting decisions (creating a new module type, introducing a new shared service) require platform-team review. SliceKit, our declarative UI framework at Reddit, is one concrete take on what feature-team-owned implementation inside a platform-team-shaped graph looks like in practice.
Pattern D works because it separates two questions that look the same and aren't: how should a feature be implemented (feature team's call) vs. how should the system as a whole be shaped (platform team's call). Most orgs collapse those into "the team owns the module." Then they spend two years rediscovering why that's wrong.
What "good day one" looks like
The first six months of a real modularization migration are platform work, not feature-team work. The deliverables aren't "X modules created." They are:
The module-type system. Decide what kinds of modules can exist. UI, Flow, Service, Logic, Feature, Feature Interface, whatever the right list for your domain is. Airbnb's October 2021 writeup describes twelve module types with strict visibility rules. Twelve is at the high end and probably overkill for most orgs. The point isn't the exact list. The point is having one at all and writing it down.
The visibility rules and enforcement. A feature module cannot depend on another feature module. A UI module cannot depend on a service. The rules are arbitrary; the enforcement isn't. If the rules live in a doc and a code review checklist, they will be broken within a quarter. If they live in the build system as compile-time errors, they hold.
The instrumentation. Spotify open-sourced XCMetrics in January 2021 for exactly this. You cannot manage a modularization migration without measuring it. Median build time, p95 build time, target count, dependency-graph depth, dev-loop time on a representative module. If you don't have these on a chart, you cannot tell whether the migration is working.
The dev loop for engineers. Airbnb's Dev Apps are the canonical example: an engineer working on a feature can build and run a workspace that contains only their module plus its dependencies, in under two minutes, instead of building the full app. Their report says a majority of local builds run through Dev Apps. If your engineers still have to build the full app for every change, you haven't done the modularization work. You've just rearranged the files.
The CI bill
The piece most modularization decks skip is the CI cost. Alberto De Bortoli wrote the honest version of this last June. The line worth quoting: "Each change involves at least 2 pull requests: 1 for the module and 1 for the integration in the app." That two-PR tax is real, it shows up in every multi-repo modularization story, and the only way to make it go away is a monorepo with selective CI.
Selective CI means: when a change lands, the build system rebuilds and tests only the targets the change actually affects, not the whole world. This is a build-system capability, not a CI script. You can't bolt it onto Xcode's default build. You can get it from Bazel or Buck. You can approximate it inside an Xcode-native build with a lot of work and a clear dependency graph.
If your CI runs the full test suite on every PR after modularization, your modularization isn't paying off yet. You took the cost and not the benefit.
The downstream payoff that justifies the cost
Modularization is expensive to do well and you cannot ship the migration in a year. So why bother.
The payoff isn't faster builds, although you get faster builds. The payoff is that other things become possible that weren't before. DoorDash's March 2021 post on building Caviar and DoorDash from a single codebase is the kind of thing you literally cannot do without first paying the modularization tax. They report roughly 90% code reuse across two brands. That is years of engineering effort saved on the second brand. You don't get there from a monolith. You get there from a modular codebase with strict visibility rules and dependency injection at the brand boundary.
This generalizes. White-label apps, regional variants, watch apps that share business logic with the phone, fast onboarding for acquired teams, easier vendor or open-source extraction. These all become available once the graph is right. They are inaccessible from a monolith.
The framing leadership wants ("modularization will speed up builds") is the small win. The bigger win is structural: the codebase becomes the kind of artifact you can do new things with.
What I'd tell a new mobile leader
Three things I would say to a mobile engineering leader who's about to greenlight a modularization project:
Treat it like infrastructure, not a feature. Multi-year roadmap, named platform-team ownership, instrumented from day one. If you can't staff a platform team for the duration, do not start. The half-done version is worse than the monolith you have today.
Pick the graph-shape and visibility rules before you cut modules. Cutting modules without a graph is just renaming files. The architectural work is upstream of any individual module split.
Plan for the CI rebuild. Modularization without selective CI is the worst of both worlds: you have all the boundary tax and none of the speed benefit. If you're not going to rebuild CI alongside it, don't bother modularizing.
The pattern across the teams that got this right is consistent. They committed to it as a multi-year investment. They named ownership at the platform level. They instrumented from day one. They didn't let "modularize" become a single-quarter bullet point on a roadmap.
The teams that treated it as a project shipped a partial version, declared victory, and spent the following year quietly relapsing as the rules degraded.
You can't ship modularization. You can only run a modularization migration, and the question is whether you set it up to compound or to rot.