Should You Modularize Your iOS App?
Modularization is one of the most discussed topics in iOS engineering right now. Conference talks, blog posts, Twitter threads — it seems like every team is splitting their app into dozens of packages. And there's no shortage of tools to do it: Swift Package Manager, CocoaPods, Carthage, Tuist, XcodeGen, even Bazel. The options can be overwhelming.
But here's the thing: the tool doesn't matter if you don't understand why you're modularizing in the first place. I've seen teams spend months migrating to SPM modules without a clear goal — and end up with a more complex codebase that's harder to maintain than the monolith they started with.
So before we talk about any tool, let's take a step back.
A Quick Overview of the Tools
If you've been around iOS long enough, you've probably used at least one of these:
CocoaPods was the de facto standard for years. It manages dependencies through a centralized Podfile, generates an Xcode workspace, and handles linking automatically. It was great for its time, but CocoaPods is now officially in maintenance mode — the team announced they would not be actively developing new features. For existing projects it still works, but it's not a foundation you want to build a new modularization strategy on.
Carthage took a more decentralized approach: it builds frameworks from source and lets you integrate them however you want. No generated workspaces, no magic — just pre-built binaries. It was popular among teams that wanted more control, but it has largely fallen out of favor due to limited community activity and the rise of SPM.
Swift Package Manager (SPM) is Apple's first-party solution. Built into Xcode, no additional tools required. It handles both external dependencies and local packages, making it the natural choice for modularization today. Most teams starting fresh will land here.
Tuist goes a level beyond: it's a project generation tool. Instead of maintaining Xcode project files manually, you describe your project in Swift and Tuist generates everything. It shines at scale — when you have 30, 50, or 100+ modules and need consistent configuration, automated scaffolding, and dependency graph enforcement.
Each tool has its place. But choosing a tool is a later decision. The first question is: should you modularize at all?
Start With the Problem, Not the Solution
Modularization is not a goal. It's a tool — and like any tool, it can be the wrong one for the job. If your app is maintained by a single team of four developers and builds in under 30 seconds, you probably don't need 40 packages. You need to ship features.
The teams that benefit most from modularization are the ones experiencing real, measurable pain. Let's talk about what that looks like.
Factor 1: Context Isolation
The most fundamental reason to modularize is context isolation — the idea that a developer working on Feature A shouldn't need to understand, compile, or even see the code for Feature B.
In a monolithic target, everything is accessible. Any file can import any other file. There's no enforced boundary between the networking layer and a specific UI screen. This feels convenient at first, but it creates invisible coupling over time. A "small change" in a utility function can break something three layers away because there's no compiler-enforced boundary telling you where one context ends and another begins.
Ask yourself: when a developer opens your project, do they need to load the entire mental model of the app to work on one feature? If yes, context isolation would help. If your codebase is small enough that any developer can understand the full picture in a day, it probably wouldn't.
Factor 2: Multi-Team Ownership
This is where modularization goes from "nice to have" to "almost required."
When multiple teams work on the same codebase, you need clear ownership boundaries. Without modules, ownership is defined by convention — "Team A owns the files in this folder." But conventions break. Someone from Team B adds a quick fix in Team A's folder. Someone else creates a shared utility that crosses ownership lines. Within months, the boundaries are blurred and no one knows who's responsible for what.
Modules enforce ownership at the compiler level. If Team A owns the Payments module, they control its public API. Team B can use it, but they can't reach into its internals. Changes to the public interface require coordination — and that's a feature, not a bug.
This becomes even more powerful when combined with a CODEOWNERS file in your Git platform (GitHub or GitLab). You can map each module directory to its owning team, so if anyone outside that team modifies code in the module, the platform automatically requests a review from the owners. Modularization defines the boundaries in code; CODEOWNERS enforces them in the review process. Together, they make ownership not just a convention but an automated, auditable workflow.
The signal: if you have 2+ teams contributing to the same codebase and you're experiencing ownership ambiguity, friction in code reviews, or unexpected side effects from other teams' changes — modularization addresses the root cause.
The counter-signal: if you're a single team, you can enforce boundaries through code review discipline and folder structure. Modules add overhead you might not need.
Factor 3: Build Times
This is the reason most people think of first, but it's more nuanced than "modules = faster builds."
Swift's incremental compilation is already quite good within a single target. If you change one file, Xcode generally only recompiles the files that depend on it. Where modules help is parallel compilation: modules that don't depend on each other can be compiled simultaneously across CPU cores.
But here's the catch — if your module graph is mostly linear (Module A depends on B depends on C depends on D), you won't see much parallelization benefit. The real gains come from wide, shallow dependency graphs where many modules can build independently.
The signal: your clean build takes more than 2-3 minutes and your incremental builds are slow because the dependency graph is too interconnected.
The counter-signal: your build times are under a minute. Or your build is slow because of a handful of complex files (heavy generics, complex type inference), which modularization won't fix.
It's also worth noting that SPM may not be the best tool when your dependency graph is large and deeply interconnected. As the number of local packages grows, Xcode's indexing and dependency resolution performance degrades noticeably — code completion becomes sluggish, syntax highlighting breaks intermittently, and resolution failures with unhelpful error messages become more frequent. This is a known pain point of SPM's integration with Xcode. If your project reaches this scale, tools like Tuist or Bazel handle large dependency graphs more efficiently. Tuist generates optimized Xcode projects from Swift manifests and gives you fine-grained control over the build graph, hitting a sweet spot between SPM's simplicity and Bazel's raw power. Bazel, Google's build system, excels at massive monorepos with extremely complex dependency trees — but comes with a steep learning curve and limited Xcode integration, making it overkill for most iOS teams. We'll explore Tuist in depth in another article in this series.
Factor 4: Testability
In a monolith, your test target typically depends on the entire app. This means running any test requires compiling everything, and it's difficult to test a component in true isolation.
With modules, each package has its own test target. You can test the networking layer without compiling the UI. You can test a feature without importing the entire app. This makes tests faster to run, easier to write, and more reliable.
But testability alone rarely justifies a full modularization effort. If your team already writes good tests and your CI runs in a reasonable time, the incremental benefit of per-module test targets might not justify the migration cost.
The signal: your test suite takes 10+ minutes, test failures are often caused by unrelated code, or developers avoid writing tests because the feedback loop is too slow.
The counter-signal: your tests run fast, you have good coverage, and adding new tests is straightforward.
Factor 5: Scalability and Onboarding
A modular codebase is inherently more navigable. New developers can start by understanding one module — its public API, its tests, its dependencies — before zooming out to the full picture. This is significantly faster than trying to comprehend a 200,000-line monolith.
Modularization also scales better organizationally. When you grow from 2 to 5 to 10 teams, modules give each team a clear scope. Without them, coordination costs grow exponentially.
The signal: onboarding new developers takes weeks. Teams are stepping on each other's toes. You're growing headcount and need the codebase structure to grow with it.
Factor 6: CI/CD and Automation
A well-modularized codebase unlocks powerful CI optimizations. Instead of building and testing the entire app on every pull request, your CI pipeline can detect which modules were affected by a change and only build and test those. This keeps pipeline times proportional to the scope of the change, not the size of the codebase.
It also enables per-module code ownership rules, selective snapshot testing, and independent release validation. But these benefits only materialize if you invest in the CI tooling to support them — modularization without CI adaptation is leaving half the value on the table.
In fact, the CI itself can — and should — be modularized alongside the codebase. Instead of one monolithic .gitlab-ci.yml (or equivalent), you can split it into smaller YAML files per module, where each job is only triggered when files in that module's directory change. This means a PR that only touches the Payments module won't trigger builds and tests for Networking, DesignSystem, or any other unrelated module. The result is a dramatic reduction in runner usage and pipeline time — you're only spending compute on what actually changed.
The Danger: Over-Modularization
Here's what nobody talks about at conferences: you can have too many modules.
I've seen codebases where teams created a new module for every small component — a module for a single utility function, a module for three data models, a module for one extension file. The result? A dependency graph so complex that adding a feature requires updating 8 package manifests, and build times didn't improve because the module overhead ate into the compilation gains.
The best defense against this is documentation. Before any team starts creating modules, write an RFC or guideline that defines what qualifies as a module, how modules should be named (with standardized prefixes or suffixes), what the dependency rules are, and how the module structure maps to team ownership. This document becomes the single source of truth that prevents ad-hoc module creation.
With clear, well-documented rules in place, you can even leverage AI coding agents like Claude Code to assist with module creation and refactoring — the agent follows your guidelines and naming conventions consistently, reducing human error. We'll explore this workflow in a future post in this series.
The Decision Framework
Here's a simple framework to help you decide:
Modularize if you check 3 or more of these:
- Multiple teams (2+) contribute to the same codebase
- Clean build times exceed 3 minutes
- Developers regularly cause unintended side effects in code they don't own
- Onboarding takes more than 2 weeks for an experienced developer
- CI test runs take more than 10 minutes
- You're actively growing the team and expect more contributors
Don't modularize (yet) if:
- You're a small team (1-4 devs) with a manageable codebase
- Build times are reasonable
- Ownership is clear through conventions that everyone follows
- No one on the team has bandwidth to define the initial guidelines and lead the first steps — modularization can coexist with feature delivery, but someone needs to own the kickoff
- The codebase complexity is manageable and developers can still navigate it without confusion
And if you do decide to modularize, don't do it all at once. Start with one or two foundation modules, measure the impact, and expand from there.
What's Next
This was the "why" and "when." In the next posts of this series, we'll get into the "how" — starting with a hands-on comparison of a monolithic project vs. one modularized with SPM, including code examples, dependency rules, and common pitfalls. After that, we'll explore when Tuist becomes necessary and how it helps enforce architecture at scale.
If you found this useful, share it with your team. Questions? Reach out on LinkedIn.