Copy logo as SVG
Copy wordmark as SVG
Download brand assets
Brand guidelines
მომხმარებლები Toss

Toss: Building less to ship faster

Toss toss.im (ნარჩუნდება ახალ ტაბში)
არსებითი 2013

The challenge

One app, many teams

At Toss, product development is organized into small, autonomous teams called silos. Each silo owns its own product end to end, and each ships into the same app. Many products were born in this environment, and along the way the iOS codebase grew to close to 1,000 modules.

Toss's iOS project already used Tuist to generate its Xcode projects and was heavily modularized: most features are split into Interface, Implementation, Model, and Testing targets. The modular structure made ownership clear, but it did not make builds fast.

Why builds were slow

The Toss team's goal was simple to state: after changing a line of code, a developer should see the result in under a minute. In practice, only a small share of local builds finished within that minute.

Profiling showed two problems. One was compilation: every change meant recompiling a large part of the app. The other was the time spent before the compiler did any real work. On every incremental build, Xcode spent a long time computing the dependency graph, planning Swift builds module by module, and creating the build description for a workspace made of hundreds of Xcode projects. A one-line change in a foundational module could trigger recompilation and Swift planning across a large part of the app.

The team tried the obvious levers first. They trimmed build scripts, removed redundant setup steps, and reduced the number of build configurations. Each helped a little. None changed the shape of the problem, because both costs grew with the number of modules Xcode had to build and consider, not with the amount of code a developer had actually changed.

The conclusion the team wrote down at the end of that phase became the thesis for everything that followed: the best way to reduce build time is to not build at all.

Choosing Tuist

A developer working on the transfers feature does not need to compile the stock trading feature or the design system. They need those modules to exist, but they can arrive already built. The team's plan was to let each developer declare what they are working on, and to turn everything else into prebuilt binaries.

Tuist was the natural fit for three reasons.

It was already the source of truth for the project graph. Because Toss generated its projects with Tuist, the graph needed to decide what to prebuild and what to keep as source already existed. Tuist's binary cache and focused generation build directly on that graph, so the team did not need to migrate to a new build system to get the benefit.

It removes the work instead of speeding it up. As long as a module stays in the workspace as source, Xcode still has to plan it and often recompile it. Turning the modules a developer isn't touching into prebuilt XCFrameworks takes them out of the build entirely.

It can be self-hosted. Tuist's server and cache can run on Toss's own infrastructure.

The approach

The project took about three months: first making prebuilt modules work on developers' machines, then sharing them across the team through a remote cache.

Making the graph cacheable

Before prebuilding could pay off, the graph itself had to change. If a module sits underneath everything, a change to it invalidates everything, prebuilt or not. The team:

  • Removed unnecessary dependencies. They automated the detection and removal of unused imports across the codebase, and moved third-party dependencies into a separate release repository.
  • Made every module cacheable. Some patterns that worked when everything was built from source broke once modules were replaced with binaries, so the team reshaped those modules until they could be prebuilt.
  • Unified build variants. Internal and production builds originally produced different binaries because of conditional compilation flags, which forced developers to rebuild the cache when switching between them. The team removed those flags from shared modules and introduced a single shared cache configuration, so one set of binaries serves both.
  • Guarded cacheability in CI. A pull request check now alerts when a change reduces the share of modules that can be cached, so regressions are caught before they reach the main branch.

A focus workflow that fits how developers actually work

On top of Tuist's focused generation, the team built a small workspace configuration layer. A developer declares the scheme they are working on in a local config file. The setup script works out which modules must stay as source, including the modules that depend on them, and Tuist generates a workspace where everything else is a prebuilt XCFramework. Inside Toss, developers can easily edit this configuration with necto, an iOS debugging tool the team recently open-sourced.

They also kept the rest of the app navigable, added safeguards against editing modules outside the focus, and gave AI coding assistants the same context about the cache, so agents follow the same workflow as humans.

A self-hosted remote cache

A local cache works, but every developer still had to warm it on their own machine after pulling new changes, which was slow. The next step was to warm the cache once on CI and share it.

Toss deployed Tuist's server on its own infrastructure. CI warms the cache that developers need, and developers pull the binaries they need. Whenever the setup needed something Tuist didn't support yet, the Tuist team usually turned it into a configuration option within days. To keep downloads fast for every team, the cache is served through Tuist's Kura cache nodes.

Scaling Tuist to Toss's graph

Moving almost the whole app to XCFrameworks solved the original problem and revealed a new one. When close to 1,000 modules are binaries, costs that are invisible in a typical project become the bottleneck.

  • Framework search paths. Every prebuilt framework adds a framework search path, and with hundreds of them, simply resolving imports becomes a measurable part of each build.
  • Hash stability. A cache is only as good as its hit rate. At this scale, small inconsistencies in how modules are hashed turn into large numbers of unnecessary rebuilds.
  • Warming cost. Building and distributing the whole graph as binaries has to stay fast enough to keep up with a constantly moving main branch.

Toss and the Tuist team worked through these together, and the fixes now ship in Tuist for everyone.

The results

For Toss's iOS developers, the experience of building the Toss app changed dramatically. Product developers now only need to build the modules they own, and switching branches or build environments no longer means rebuilding everything. In this setup, working in example apps became even more productive.

  • Builds within one minute more than tripled as a share of local builds.
  • Most of the graph is served from cache: most modules can be cached, and most cache warms are served from the remote cache.

Faster builds also had a big impact on AI agents. Agents verify their changes by building too, so as builds got faster, development with AI got much faster as well.

What's next

For Toss, faster builds were never the end goal. They are one part of giving iOS developers the best possible development experience, so that they can focus on the problems that really matter for their product. Being able to use prebuilt binaries through Tuist was a huge change to that experience, and the team will keep investing to build the best possible development environment.

What are you waiting for?

ხელოვნური ინტელექტი სწრაფად ვითარდება. ასევე უნდა სწრაფიდეს თქვენი აშენებები.

დაწყება ესაუბრეთ ჩვენ