How do you design the dependency graph, handle shared resources, and optimise build times when breaking down a massive iOS monolithic application into a modular architecture using Tuist?
Assess the candidate's ability to migrate a large-scale iOS application to a modular architecture using Tuist, managing dependency graphs, enforcing architectural boundaries, and optimising build times. Use this MOBILE answer to show the decision, trade-off, and evidence rather than a memorised definition.
What the interviewer is scoring
- Whether they understand the strategy for extracting domain modules without breaking the app.
- Does the candidate effectively utilise Tuist to manage the dependency graph and project generation?
- That they address the challenges of resource sharing and dependency injection across modules.
- Whether the candidate considers the impact of dynamic versus static linking on launch times and app size.
Answer
Short answer
Modularising an iOS monolith with Tuist starts by extracting vertical feature targets, defining one-way dependency rules, isolating resources, and using project generation plus caching to reduce build times without recreating the monolith as tangled frameworks.
Why ad hoc frameworks recreate the monolith
Massive, legacy iOS applications are often built as single monolithic Xcode projects. Codebases exceeding a million lines of Swift and Objective-C inevitably suffer from unmanageable build times, frequent merge conflicts in .xcodeproj files, and a tightly coupled architecture that grinds feature development to a halt. The naive approach to modularisation involves creating a few shared utility frameworks and hoping the problem resolves itself. This invariably fails. Without strict boundary enforcement, the monolithic dependencies simply bleed across module boundaries, resulting in a distributed monolith where build times actually regress due to excessive dynamic linking overhead.
Designing the dependency graph
Untangling a monolith requires defining a rigorous strategy for the dependency graph without halting ongoing feature development. A vertical slice approach—extracting standalone features into separate targets—is generally superior to extracting horizontal technical layers.
When configuring Tuist's Project.swift files to define these targets, the linking strategy is a critical decision point. Linking everything dynamically increases the app's launch time due to the dyld dynamic linker overhead. Conversely, linking everything statically increases the binary size and can lead to symbol duplication if not managed carefully. The pragmatic strategy utilizes static frameworks for internal modules while keeping external heavy dependencies dynamic where appropriate, leveraging Tuist's capabilities to manage these linkage semantics automatically.
Shared resources and injection
Managing shared resources—such as images, fonts, and localised strings—across modules is a common pain point. Utilizing Tuist's synthesised resource interfaces allows modules to encapsulate their own resources. This prevents the main app bundle from becoming bloated and ensures that each module is truly self-contained.
Furthermore, introducing a dependency injection container that operates across module boundaries is essential. This ensures that modules remain decoupled and only communicate through well-defined public protocols, hiding their internal implementations and preventing concrete dependencies from leaking.
Taming the legacy code
Mixed Swift and Objective-C codebases introduce significant friction. Bridging headers do not scale in a highly modularised environment. Legacy Objective-C code must be strategically transitioned into isolated modules, exposing only Objective-C framework umbrellas, or critical paths must be refactored into Swift to ensure seamless interoperability. Tuist's graph commands are invaluable here for visualising dependencies and eliminating implicit links that were previously masked by Xcode's permissive build system.
Enforcing boundaries and caching
Developer workflow must be safeguarded. Leveraging Tuist's caching capabilities to cache pre-compiled binaries of stable modules drastically reduces incremental build durations. The CI pipeline should utilise tuist cache warm to distribute the cache across the development team.
However, developers will inevitably attempt to introduce circular dependencies between feature modules. Strict dependency rules must be implemented using Tuist's graph validation features, ensuring that feature modules can only depend on core or shared modules, and never on each other directly. Creating custom Tuist plugins to add linting rules to the project generation process prevents invalid dependency edges from being introduced in the first place. Finally, setting up micro-apps for each feature module allows developers to build and test features in isolation, radically improving developer velocity.
flowchart TD
A["Main App Target"] --> B["Feature Module A"]
A --> C["Feature Module B"]
B --> D["Core Networking"]
C --> D
B --> E["Shared UI Components"]
C --> E
D --> F["Foundation Utilities"]
E --> F
A --> G["Dependency Injector"]
G --> B
G --> CModularising a large iOS monolith requires a delicate balance between untangling legacy code, managing the dependency graph, and optimising build performance through tools like Tuist, ultimately leading to improved developer velocity and a more maintainable architecture.
© 2026 Preptima. Originally published at preptima.com.
Likely follow-ups
- How do you prevent two feature modules from silently coupling to each other's internals over time?
- What is your rollback plan if a module boundary you defined turns out to be wrong six months into the migration?
- How do you keep Tuist's generated project in sync with a large team making frequent structural changes without constant merge conflicts?
Related questions
- How do you architect and implement Dynamic Feature Modules in an Android application to reduce initial download size and deliver features on demand?hardAlso on mobile and architecture3 min
- You're asked to break up a five-year-old iOS app that has become one enormous Xcode target - how would you approach modularising it, and what does it cost you?hardAlso on ios and architecture4 min
- How do you leverage WorkManager to guarantee execution, handle conflict resolution, and optimise battery usage in an offline-first Android application requiring bidirectional synchronisation?hardAlso on mobile and architecture3 min
- How do you resolve severe performance bottlenecks in a complex React Native application caused by heavy bridge traffic, and how do you evaluate a migration to the JavaScript Interface (JSI)?hardAlso on mobile and architecture3 min