DEV Community

Oleh Veheria
Oleh Veheria

Posted on

Modularizing a Legacy iOS App Without Stopping Delivery

I spent a year migrating a smart-pet-device company's app off a single-target Objective-C codebase, module by module, while the team kept shipping releases the entire time. No feature freeze, no six-month rewrite branch nobody could merge back. +30% on performance and UX metrics by the end, and every release in between still went out on schedule.

Modularization gets sold as a build-time problem: split the app so Xcode compiles faster. That part's real, but it's not what makes it hard. The hard part is deciding where the boundaries actually go, because a wrong one costs you for years, and the stop-the-world rewrite to fix it later is exactly the thing you were trying to avoid.

Boundaries follow ownership, not folders

The obvious move is modularizing along the existing folder structure: pull Networking/ into a module, then UI/, then Models/. That produces modules that compile a bit faster and change nothing else, because the coupling that used to be implicit inside one target is now explicit across module boundaries, and just as tight.

The boundary that actually held on this migration was the one that followed who owned the code and how often it changed independently: a screen's data layer, its business logic, and its UI moved together as one unit, because that's the unit a single engineer or pair actually shipped changes to. A "shared utilities" module everyone imports and nobody owns is where modularization efforts go to die. It becomes a second monolith with extra ceremony bolted on.

Extract the smallest thing first, not the biggest

The natural first move is pulling out the biggest, most obviously reusable piece: the design system, the networking layer. Those are the wrong first extractions, because they're also the pieces with the most existing call sites, so proving the approach means touching the most files before you've proven anything.

The extraction that actually de-risks the migration is the smallest, most isolated feature module with the fewest dependents. Get its public interface defined, its internals hidden, and ship it in a real release. That's the proof the rest of the migration reuses. Start with the design system instead, and when something breaks in week three you can't tell whether it's the modularization approach or the size of the diff.

The interim is the project, not a phase you pass through

A half-modularized app has a monolith target and a growing set of extracted modules depending on each other in ways nobody fully mapped. Every new feature during that stretch has to decide whether it belongs in the module structure or the legacy target, and whether that decision gets revisited when the module graph shifts next month. Budget the interim with an explicit exit criterion. "60% modularized" isn't a real number when nobody's diagrammed the dependency graph it's measured against.

What actually breaks delivery mid-migration

Build times regress briefly while modules settle into their real dependency graph. Merge conflicts cluster in whatever file every module still touches, usually the app's root composition point, until that gets its own clear owner too. And team confidence dips: engineers who've shipped fast in the old monolith for years feel slower for a few sprints inside the new boundaries, before the structure starts paying that speed back. Name that dip to the team before it happens, not after someone asks why velocity looks worse this sprint.

The discipline that holds

Define a module's public interface before extracting its implementation. Ship one small module fully, in a real release, before touching a second. Track the interim as its own workstream with an exit criterion. And accept that the ownership map, not the folder structure, is what decides where a boundary actually goes.

If you're staring at a monolithic codebase and a roadmap that can't pause for a rewrite, that's exactly the kind of call I help teams make in a strategy session.

What's the module in your own codebase that everyone agrees should be split out, and what's actually stopping the team from starting?


Originally published at veheria.tech/blog/modularizing-legacy-ios-app.

Top comments (0)