This writing is the result of my experience at Airbnb and building cmd, an AI assistant for Xcode. You can find here the code described in this post. It is open source as part of cmd. While not battle tested, it has powered cmd for a several months with great success.
This post highlights the benefits of modularization, but generally assume you are already interested in modularizing. It then shows a system to simplify configuration within SPM to the point where you barely touch any config file at all, and presents a way to load only part of your app in Xcode.
Why modularize?
At Airbnb I enjoyed the power of a highly modularized codebase. This was the core ingredient to scaling our app and organization. Modularization has three types of benefits that help with scaling:
Performance. Faster build (parallelization, caching), faster tests (don’t build the whole app) etc. Airbnb had a p90 build time under 1mn, a remarkable feat for such a large app (it took over 1000 modules and lots of other optimizations).
Organizational clarity. It’s easier for several teams to manage their scope. Modules clarify ownership boundaries. They make it easy to define various project specific policies (test coverage, reviews process etc)
Reduced cognitive load. This one is often overlooked – maybe because less measurable – and my favorite. When each piece of code has clear boundaries and you only have to think in detail (e.g. private/internal) about a few tens of files, it’s much easier to reason about the code you’re writing and its blast radius. Limited context windows is not just a concern for LLMs!
Modularization also supports creating focus projects that load only part of the app in Xcode. More on that later.
While those issues might seem relevant only to large companies or massive apps, that’s not the case. Even at a medium scale, where your codebase might not be crying out for better structure, good modularization can still make a huge difference — helping you work faster and more efficiently.
What does good modularization look like?
It satisfies 3 criteria:
Favors a shallow dependency tree. This will bring a lot of performance improvements as each module, except for the app target, needs little dependencies to be built for it to work and you get great parallelization.
Uses dependency injection. In addition to facilitating the above, DI is a great infrastructure for unit testing.
Defines a few opinionated patterns. This brings clarity to all the contributors – including AI agents – on how they should organize code, and facilitate decision making.
I found Uber’s RIBs to be a great inspiration for how to modularize a codebase, but your mileage may vary.
Modularization at cmd
Here’s what I’ve been using for cmd:
Examples of Services would be “NetworkingService”, “CodeCompletionService”, “AppUpdateService”. They do the work that is typical of a library – maybe a better name! – and have no UI component. They are all dependency-injected through swift-dependencies and the consumer only needs to know about the interface.
Examples of Features would be “ChatFeature”, “SettingsFeature”. They usually correspond to one screen, sometimes a few. They are dependency-injected through a router and the consumer only needs to know about the route parameters.
This might seem like a good amount of overhead, but when “creating a module” is as simple as “creating a folder” the code is easily organized.
What’s the problem with SPM?
Honestly, I think SPM has a lot of shortcomings, which is regrettable for a modern package manager: no lock file, no global registry, very slow version resolution, and a limit of just one public package per repo.
Package.swift is a single config file that keeps growing and growing the more modules you have [1]. With 70 modules — more than most but not unreasonable — cmd is clocking 1300 lines on its Package.swift. This is un-readable, un-maintainable.
It’s unfortunate. At its core, modularization is dead simple: a folder is your unit of code; everything imported is a dependency. Can we make it that easy?
Module.swift
Step 1: write a Module.swift
Instead of a single shared Package.swift, each module defines its dependencies in a local config file.
// - LocalServerServiceInterface
// - Module.swift
// - Sources/
// - Tests/
Target.module(
name: “LocalServerServiceInterface”,
dependencies: [
.product(name: “Dependencies”, package: “swift-dependencies”),
“AppFoundation”,
“ConcurrencyFoundation”,
“JSONFoundation”,
],
testDependencies: [
“LocalServerServiceInterface”,
“SwiftTesting”,
])This is not a novel idea: Bazel, Buck, and others all do this. Your module’s configuration is colocated with the code it describes. Gone is the unmanageable shared file. Module.swift is also quite concise.
You’ll notice the Target.module method. This is a helper that abstracts common patterns, such as having tests attached to a module. It worked well for cmd. Not required.
Ok but… we still want a Package.swift for all the Swift tooling to work.
We just need to automate some aggregation. We convert a local template to a standard Package.swift. That template is where Target.module is defined, as well as external dependencies.
extension Target {
static func module(...
}
var targets = [Target]()
// copied from each Module.swift
targets.append(contentsOf:
Target.module(
name: “LocalServerServiceInterface”,
...))
//
let package = Package(
...,
dependencies: [
.package(url: “https://github.com/pointfreeco/swift-dependencies”, from: “1.9.0”) // External dependencies defined here
],
targets: targets)Quite straightforward. Like Tuist uses a more developer friendly format to generate xcodeproj, we use a new one to generate Package.swift.
Step 2: don’t write a Module.swift
Didn’t we say “Everything imported is a dependency”? Most of the information in the Module.swift can be derived from the code. All you need to do is to create a Module.swift to signal that this folder is a specific piece of code that should be modularized.
If there’s a non empty Test/ folders, we have tests, otherwise we don’t.
If there’s an import AppFoundation, we depend on AppFoundation, otherwise we don’t.
So our script does just that. It fills and updates the Module.swift with the required properties, and you can make manual adjustments for things like resources, build flags etc.
We now got to the point where creating and maintaining modules doesn’t add much overhead over creating directories.
At cmd, all I have is a file watcher running:
cmd sync:dependenciesHere’s how it looks in practice: we’ll create a new module and reference it as a dependency from an existing module. All without touching any Package.swift / Module.swift:
Focussed Package
Load less code in Xcode
cmd focus --module LocalServerServiceInterface --openLet’s reap more rewards.
We talked about aggregating the Module.swift into the shared Package.swift. We can also expand a specific Module.swift to its own Package.swift, including only its dependencies.
When we open this focussed package, we load a much smaller code surface into Xcode. This means faster indexing, faster builds, faster tests, and much higher chances of SwiftUI Previews working like in a WWDC demo. This is a great workflow when iterating on a single module. One that we get for free.
Our script generates this focussed Package.swift. It either opens the package in Xcode or prints its location for other uses.
cmd focus --module BuildTool --openFocussed Package → fast, reliable previews:
Entire app → Previews rarely work:
Gotchas & misc
Module.swift and the code that powers it at cmd has not been extracted to a dedicated repository to facilitate its adoption. If you’re interested, don’t expect a turn-key solution at this point even though this could happen if there’s interest.
I noticed that Xcode was getting really confused when my project’s directory would contain .build folders unrelated to the main Package.swift it knows about (those are from prior uses of focused package). In such cases, it would not load most files. To resolve this I had to clear the .build folders when switching between focus projects and the main app, which is unfortunately wasteful.
At cmd, I decided to track the main Package.swift (I could likely get away without as it’s been reliably generated) but not each Package.swift that corresponds to an expanded Module.swift. It’s been a good balance.
[1] When you start to have a few tens of modules, you’re faced with one decision to make: do I keep all of them in a single Package.swift, or do they each get their own Package.swift and use local dependencies?
I’ve found the latter to not be workable. You get a lot of .build folders, your modules don’t share build caches, running your test suite takes a very long time, it’s a lot of Package.swift to manage — and they are somewhat verbose.
As we’ll see, the solution proposed both actually enables both setups.
A note on cmd: cmd is an AI assistant dedicated to iOS/macOS engineers who work in Xcode. It provides intelligence superior to that of Apple, is open source and provides a number of features such as AI provider selection, MCP, chat modes, and more to come.
It is free to use and works on a bring your own API key basis.



Amazing write up, thanks so much for sharing.