Question 5
Feature modules, SPM packages, and build graphs
Build times are growing and teams step on each other in one Xcode target. What modularization strategy would you propose, and what mistakes make modules worse?
Follow-ups
- How do you prevent circular dependencies?
Answer outline
Split by feature verticals (SearchFeature, CheckoutFeature) plus shared kits (DesignSystem, Networking, Analytics). Each SPM target exposes only public types at its boundary — implementation details stay internal.
- 1.Merge conflicts in the shared
.pbxprojshrink significantly — feature teams rarely touch the same targets. - 2.Incremental build times drop — Xcode only recompiles targets downstream of your change.
- 3.Accidental cross-feature coupling is caught at compile time rather than in code review.
Common mistakes: cyclic imports between feature modules, a shared Utils target that becomes a dumping ground, and modularising too early before the team pain justifies the overhead.
Principles
- Enforce a one-way dependency direction: Features → Domain/Core → Infrastructure — nothing flows back up.
- Only
publictypes cross module boundaries; keep implementation detailsinternalto preserve the seam. - Avoid a catch-all
Utilsmodule — split by concern (DesignSystem,Logging) so each target has a clear purpose. - Prevent circular dependencies by placing shared contracts in a low-level
DomainorCoreTypestarget both sides can import.
Features depend on Domain and shared kits; nothing in Domain or kits depends on a feature:
let package = Package(
name: "AppModules",
targets: [
.target(name: "Domain"), // no app dependencies
.target(name: "Networking",
dependencies: ["Domain"]),
.target(name: "DesignSystem"),
.target(name: "ProfileFeature",
dependencies: ["Domain", "Networking", "DesignSystem"]),
.target(name: "SearchFeature",
dependencies: ["Domain", "Networking", "DesignSystem"]),
]
)
Follow-up angles
- Circular dependencies: introduce a
CoreTypesorSharedInterfacestarget that both sides import — neither depends on the other. - For stable shared kits, pre-built
XCFrameworkbinaries skip recompilation entirely and can cut clean-build times significantly.