Scalable Modularization with Tuist

 How to make your iOS app grow in a structured and maintainable way


Introduction

When an iOS app grows, the codebase usually grows messy too. Build times get slower, dependencies get tangled, and adding new features starts to feel painful. If more developers join the team, things only get worse — everyone is stepping on each other’s toes.

The solution? Modularization. Breaking the app into smaller, independent pieces makes development faster, testing easier, and code more reusable across projects. It also gives the team clear boundaries, so people can work in parallel without creating a giant spaghetti monster of code.

In this article, I’ll go through why modularization matters, the idea of uFeatures as a way to organize modules, what Tuist is, and how I use it to keep projects scalable. The SmartShop project will be the running example.

Why Modularization Matters

When your project is small, everything in one place feels fine. But as the app grows, so do the problems: builds get slower, dependencies get messy, and adding new features becomes risky. That’s where modularization comes in.

Breaking an app into smaller modules has a few clear benefits:

  • Faster builds → Only the changed module needs to be rebuilt.
  • Clear boundaries → Each module has its own responsibility, so it’s easier to reason about the code.
  • Parallel development → Different teams (or developers) can work on different modules without blocking each other.
  • Reusability → A well-built module can be used across multiple apps or projects.
  • Easier testing → You can test a feature in isolation, without pulling the whole app into memory.
  • Scalability → As the codebase and the team grow, the project doesn’t collapse under its own weight.

In short: modularization keeps your app healthy as it grows. It’s not just a “nice to have” — it’s the foundation for scaling without pain.

The Concept of uFeatures

One of the hardest parts of modularization is deciding what a module should be. If modules are too big, you lose flexibility. If they’re too small, the project gets over-engineered. That’s where the idea of uFeatures (unit features) comes in.

uFeature represents a self-contained feature of the app. Think of it as a mini-project: it has its own interface, implementation, and tests. It has an specific structure:

  • Interface → Defines what the feature exposes (protocols, models, contracts).
  • Implementation → The actual code that makes the feature work (views, services, logic).

Why split them? Because dependencies should only point to interfaces, never to implementations. That way:

  • Each feature depends only on what another feature promises to deliver, not on how it does it.
  • Implementations stay private, reducing coupling between modules.
  • The dependency injection system is the only place that sees the “big picture” and wires everything together.

And here’s the crucial part: this separation is what makes builds faster.

When you change something in the implementation of one module, other modules don’t need to rebuild — they only care about the unchanged interface. The compiler has less work to do, and build times stay under control even as the project grows.

In short:

  • Interfaces are contracts → lightweight, stable, and easy to depend on.
  • Implementations are details → isolated, swappable, and invisible to the rest of the system.

This pattern keeps your app modular, scalable, and efficient. Without it, modularization quickly turns into a tangled mess that compiles like a monolith.

What is Tuist

Ok, modularization sounds great — but anyone who tried to manage a dozen Xcode modules manually knows it’s a nightmare. Creating targets, wiring dependencies, updating schemes… it’s repetitive, error-prone, and doesn’t scale.

That’s where Tuist comes in.

Tuist is an open-source tool that lets you define your project structure in Swift code and then generates the Xcode project automatically. But it goes way beyond that. Tuist is not just about generating projects — it’s about making the whole development workflow smarter and more scalable.

Here’s what makes Tuist powerful:

  • Project generation → No more dragging files in Xcode or fixing broken project settings. Everything is defined in code, versioned, and consistent.
  • Focus mode → You can “focus” on a specific module (a uFeature, for example), and Tuist will generate a lightweight Xcode project containing only that part. This keeps builds and indexing blazing fast.
  • Selective testing → In CI, you don’t need to run the entire test suite. Tuist can figure out which tests are impacted by your changes and run only those. That means faster pipelines and happier developers.
  • Smart caching → Tuist can cache pre-built modules and reuse them across builds. If you didn’t change a module, you don’t pay the price to rebuild it.
  • Scalability → Adding new modules or updating dependencies is just a few lines of Swift code. No manual Xcode ceremony.
  • Team friendly → Because the project is generated, there are no more annoying git conflicts on .xcodeproj files.

In short: Tuist makes modularization practical. Without a tool like this, the complexity of managing dozens (or even hundreds) of modules could easily outweigh the benefits.

My Approach with Tuist

My idea was to bring the concepts of uFeatures into Tuist in a way that scales better. By default, Tuist projects usually rely on a single Project.swift file where all modules and targets are declared. That works for small projects, but it doesn’t scale well as the number of modules grows.

My approach is different: each uFeature has its own Project.swift file that defines its behavior and dependencies. This keeps every feature self-contained and makes the overall project easier to grow and maintain.

But I didn’t stop there. I also built an abstraction layer on top of Tuist to simplify how uFeatures are declared. Instead of writing verbose target definitions every time, I enforce a clear convention for folder structures, and then provide helpers that generate the targets in a consistent way.

The convention looks like this:

uFeature/
  Interface/
    Sources/
    Tests/Sources/
  Implementation/
    Sources/
    Tests/Sources/
  • Interface/Sources → contains protocols, contracts, and DTOs.
  • Implementation/Sources → contains the concrete logic, views, and services.
  • Tests/Sources → each side (interface or implementation) can have its own dedicated tests.

With this layout, adding tests is just a matter of declaring them in the Project.swift file inside the feature. The abstraction layer takes care of boilerplate, so the Project.swift for a feature stays short and predictable.

Example (uHome/Project.swift):

import ProjectDescription
import ProjectDescriptionHelpers

let project = Project(
    name: Feature.Home.rawValue,
    targets: [
        .feature(
            interface: .Home
        ),
        .feature(
            implementation: .Home,
            dependencies: [
                .interface(.Networking),
                .interface(.Home)
            ]
        ),
        .test(
            implementation: .Home,
            dependencies: [
                .interface(.Home)
            ]
        )
    ]
)

This structure makes modularization with Tuist not just possible, but scalable and practical. Each feature owns its own definition, dependencies stay clean (interfaces only), and the boilerplate is reduced thanks to conventions and helpers.

The App module as the composition root

All uFeatures are designed to be independent, but they need to be wired together somewhere. In my approach, that place is the App module.

The App module contains the core code (like the AppDelegate) and it is also the composition root where dependency injection happens. Unlike uFeatures, the App module can “see” all other features — both interfaces and implementations — and is responsible for wiring them up.

Example (uApp/Project.swift):

import ProjectDescription
import ProjectDescriptionHelpers

let project = Project(
    name: Feature.App.rawValue,
    targets: [
        .makeApp(
            name: "SmartShop",
            sources: [
                "Core/**"
            ],
            dependencies: [
                .interface(.Home),
                .implementation(.Home),
                .interface(.Networking),
                .implementation(.Networking)
            ]
        )
    ]
)

This way, the app holds the big picture, while each uFeature stays clean and independent.

Automations with CLI / Makefile

Another advantage of using Tuist is the ability to enforce standards and repeatable workflows. In SmartShop, I added a simple Makefile that wraps common Tuist commands into easy-to-remember tasks.

Examples:

  • make dev → ensures Tuist is installed, generates the project, and opens it in Xcode.
  • make test → always runs tests using a specific iOS version and simulator, guaranteeing consistency across machines and CI.

This kind of automation reduces friction for developers. Nobody needs to remember long commands or worry about setup differences — everything is just one make away.

Focus Mode

When you’re working on a project with millions of lines, opening Xcode can be a challenge. By using Tuist with modularization, you can focus only on the parts you want to work on:

tuist generate Home

When you generate the project while focusing only on the module you want to work on, Tuist will build the project with a cache of that module’s dependencies and create an Xcode project containing only your module’s code:

Conclusion

Modularization is the foundation for building iOS apps that can actually scale. By splitting features into uFeatures with clear boundaries between interface and implementation, you get faster builds, safer dependencies, and a codebase that doesn’t collapse as the team and product grow.

Tuist makes this whole setup practical. It removes the pain of managing dozens of Xcode targets manually and adds smart features like Focus modeselective testing in CI, and caching, which are essential for keeping development smooth at scale.

My approach with Tuist goes one step further: each uFeature defines its own Project.swift, follows a strict folder convention, and stays lightweight thanks to helper abstractions. The App module acts as the composition root, wiring everything together. On top of that, automation through a simple Makefile keeps the developer experience clean and predictable.

If you want to see this in action, check out the SmartShop repository.

And if you try this approach in your own project — or if you’d just like to chat more about modularization and Tuist — feel free to reach out on LinkedIn.