Designing for Productivity in a Large-Scale iOS Application

Michael Bachand
The Airbnb Tech Blog
12 min readOct 5, 2021

--

How innovation in technology and people processes have enabled iOS developers to remain productive in a large codebase.

Every iOS engineer remembers the joy of seeing their first application running on an iOS device. The human-centric interface of the iPhone brings the program to life. When you choose iOS development as a career, that joy grows as your application touches more people’s lives.

Affecting more users often involves new iOS features, flows, and functionality. But as an application grows to serve more users, new features and functionality can introduce additional weight and complexity, which slows product iteration and precludes atomic refactors.

We have undergone this journey as an iOS team at Airbnb. There is a profound joy knowing that the code we ship every week enables unforgettable vacations for guests and new revenue streams for host entrepreneurs. A focus on design is in our DNA and we take immense pride in perfecting every detail of what we show to our users. At the same time we have not been immune to the challenges of developing at scale.

In this article, we will walk through difficulties that we have encountered in accommodating the growing business needs of Airbnb. We will outline how investments in technology, ownership, and processes have allowed the Airbnb iOS team to have the best of both worlds: working in a globally impactful codebase without feeling its weight.

Challenges of developing a large-scale iOS app

An Airbnb intern made the first commit to our iOS application on June 16th, 2010. Since then, that same Xcode project has evolved into a codebase with 1.5 million lines of first-party code. About 75 iOS engineers work on our application today. We ship the app weekly in 62 languages supporting a community of guests and hosts in nearly every country on the planet.

At Airbnb’s scale, code organization becomes a challenge. We welcome and encourage experimentation and new ideas. Then, once we’ve sufficiently explored a solution space, we value the consistency of an “Airbnb way.” Until recently, most of our code was organized into modules within a flat directory called lib/. Without any hierarchy or categorization of our code, it became hard for engineers to find the existing implementations of general-purpose capabilities. We began to notice many ways to accomplish the same task in our application code, which bloated the binary that we shipped to users. Moreover, we found that each competing implementation of the same capability tended to be of lower quality than one implementation that received more investment and attention.

While Xcode is the tool in which iOS engineers feel most comfortable working, we found that Xcode does not scale gracefully to a codebase of our size and complexity. Not only are Xcode project files challenging to review in pull requests, but the incidence of merge conflicts and race conditions in these project files increased with a larger team of engineers moving at a high velocity. Even opening Xcode can become a chore with a large codebase. Over a year ago, we measured that Xcode would take between one and two minutes to become interactive when loading a workspace with all of our source code.

A pull request circa 2018 adding one new module. Over half of the changes to the pbxproj Xcode project file are not shown in this screenshot.

Most iOS engineers became frustrated with long build times and slow iteration loops. At one point, a particularly creative engineer found that his laptop would compile code faster if he unplugged his external monitor. Many engineers have trained themselves to plug the USB-C charging cable into the right side of their MacBook Pro to avoid the productivity loss of thermal throttling. When Airbnb’s codebase was less than 500k lines of first-party code, some of these problems could be rectified with more powerful hardware, though we identified a practical limit to that solution as well. It became hard to feel like you were doing your best work when large amounts of your day were spent waiting for builds to complete.

These challenges grew organically. Feedback from new hires provided valuable input for how we should prioritize infrastructure needs, as iOS engineers who came from companies with smaller projects had not yet become used to the sluggishness and workarounds of an overgrown codebase. We knew that something had to change to ensure that Airbnb continued to ship a world-class iOS application.

Solutions we’ve implemented

We investigated and implemented many solutions over the years to solve the problems stated above. In this post we will discuss the three biggest levers that have allowed us to operate efficiently at scale. We expect that these high-level themes will be applicable to other small- to medium-sized iOS teams undergoing rapid growth.

Adopting a modern build system

Xcode remains the preferred IDE for iOS engineers at Airbnb. At the same time, we’ve seen features in other build systems that we knew could improve the productivity of iOS developers. A few stood out: network caches of build artifacts, a query interface for the build graph, and a seamless way to add custom steps as dependencies. We believe that these capabilities are table stakes for a modern build system.

Facebook’s Buck build system met these requirements. We began discussing Buck seriously in late 2016 and began explorations in earnest in 2017. In 2019, we fully transitioned to Buck’s declarative build system. We have benefited greatly from Buck though we found that public documentation left much to be desired. Accordingly, we have shared our Buck setup in a public GitHub repository.

As part of this transition, we removed our manually managed Xcode projects in favor of declarative BUCK files, which live adjacent to each module’s code. BUCK files are defined using the Starlark language, which is interoperable with Bazel, another popular modern build system. Below is the structure of an existing Airbnb module.

~/apps/ios/features/WifiSpeedFeature> tree -L 1
.
├── BUCK
├── Sources
├── Tests
└── _infra
3 directories, 1 file

Our infrastructure teams have taken the approach that we should meet engineers where they’re at while supercharging their development experience under the hood. Accordingly, iOS engineers continue to develop in an Xcode workspace that is generated from the build graph defined in Buck.

Initially, only our command line Buck builds of the Airbnb iOS application could benefit from the Buck HTTP cache. This alone was a great improvement since it enabled us to validate that an App Store build would succeed on every pull request without slowing down engineers. Local Xcode builds, however, could not pull artifacts from the cache as the generated Xcode workspace continued to use the standard Xcode build system.

We have continued to leverage the modern build system at the foundation of our application to tighten the iteration loop for engineers. We made it possible to generate an Xcode workspace that internally builds the application using the Buck build system. All of the standard Xcode tools (breakpoints, console, errors) that iOS engineers use every day work as expected.

The Buck-based Xcode workspace improved local build speeds as it can participate in Buck’s cache. To improve the launch time of Xcode, we also made it possible to generate an Xcode workspace with only a subset of our entire codebase. The entire application continues to be built with Buck, and Xcode becomes interactive in a fraction of the time.

Designing module types

To address the lack of hierarchy in our code, and therefore lack of discoverability, we have designed an organizational structure for our first-party code. Modules are organized into semantically meaningful groups, called module types.

We have written precise documentation for our module types. Since the concept of module types is so fundamental to the way iOS developers work at Airbnb, this documentation is hosted on our internal developer portal and managed in source control. We summarize each module type in just a few paragraphs, explaining the purpose of the module type and the types of code it was designed to support.

We considered both application programming and build system best practices when designing this architecture. Each module type has a strict set of visibility rules. These visibility rules define the allowed dependencies between modules of that type. An individual module may tighten its visibility, a technique used by some larger teams who enjoy the benefits of modularity and want to avoid unexpected inbound dependencies on their modules. An individual module cannot expand its visibility beyond the limits imposed by its module type.

Let’s look at an example…

A feature is one of our core module types. At Airbnb, features are user-facing destinations. In our iOS code, a user-facing destination is a UIViewController that will be presented modally or installed into a UINavigationController. A feature module should be scoped to a single user-facing destination when possible, though it may contain multiple UIViewControllers that implement the destination.

Feature modules are not visible to other feature modules (i.e. a feature module cannot depend on a feature module); however, they share lightweight types via a sibling module type called a feature interface.

Each feature has a corresponding feature interface, which has broader visibility. A feature can depend on any number of feature interface modules and always depends on its own interface module. The interface functions similarly to how header files function in Clang programs.

The visibility rules of the feature module type ensure that all feature modules are independent of each other. The interface module type allows features to share simple types (protocols, enumerations, value types), enabling capabilities like strongly typed routing between features.

In addition to the feature module type, the service module type is home to non-UI objects that are responsible for managing state that is shared between features. Any service module may optionally have an interface sibling module as well.

We have twelve iOS module types at Airbnb today.

Our semantically meaningful module types act as a table of contents for our very large codebase. Engineers immediately have a reasonably accurate mental model for a module based on its type. 90% of our first-party code has been migrated from lib/ to module types. A great talk by my colleague Francisco describes in greater detail how our code organization strategy evolved from folders to module types and also sheds light on how we operationalized this large migration.

Creating Dev Apps

Our investments in build systems and iOS application architecture enabled a third innovation: Dev Apps. A Dev App is an on-demand, ephemeral Xcode workspace for a single module and its dependencies.

Dev Apps originated in the Airbnb Android ecosystem. The popularity and success of both Android and iOS Dev Apps derive from a simple axiom: minimizing your IDE scope to only the files that you are editing tightens the development loop. When there is less code in your Xcode workspace, Xcode can index and compile that code more quickly.

Adopting module types in our codebase broke costly dependencies between functional units. Now modules have minimal dependencies. For example, building any feature module and all of its dependencies is always much cheaper than building the entire Airbnb application. Since feature modules cannot depend on other feature modules, we have defined away the possibility of mega features that transitively build the entire application.

iOS engineers create Dev Apps using a robust and user-friendly command line interface. The command to generate a Dev App follows Unix best practices with a focus on being accessible to engineers who may not be comfortable in Terminal. Under the hood the tool uses Buck’s query interface to assemble the full list of source files.

The Dev App command line tool generates a container iOS application to host the feature and opens a generated Xcode workspace. Developers define variants of their feature in non-production code. These variants enable one-tap access to any possible UI state. The Dev App container application provides conveniences for common workflows, like attaching an OAuth token to HTTP requests.

A Dev App for an existing Airbnb module. Developers can test all states of their feature by defining variants (left). Developer settings support live network requests (right).

A Dev App allows a product developer to iterate on their feature’s UI and much of its business logic while building a fraction of the overall Airbnb application. Although Dev Apps were designed for feature and UI modules, we now support creating a Dev App for any module type. We have found that many iOS developers also prefer to work on non-UI modules in this minimal Xcode environment.

It remains critical to build and run the entire Airbnb application in many situations, especially for testing how features interact with each other. However, when you are working on code that is sufficiently isolated and well-tested, it can be possible to ship that change confidently with a Dev App alone.

Breaking through to the other side

Our efforts have created an ecosystem where teams can operate independently on their surface areas. Coding in a Dev App recalls the joy of coding in a simpler, smaller project with all of the benefits of being supported by mature first-party tools and frameworks.

Dev Apps now drive over 50% of local builds. The 75th percentile of Dev App build times is under two minutes, with the 50th percentile well under one minute. We encourage Dev Apps to use mocked dependencies as much as possible to reduce the code required to be built.

Our application architecture has ushered in an era of 100% code ownership. We have maintained 100% ownership through team reorganizations due in part to our highly modularized codebase, which allows the ownership to be transferred and repartitioned with minimal refactoring. Today, our first-party code is divided into nearly 1,500 modules.

We see over 50% test coverage for code in our modern module structure while code that remains in the legacy module structure has 23% coverage. This is in part due to interface modules, which strongly push developers to write services using the protocol-oriented programming technique. When code interacts primarily with protocols, it is easy to create test doubles for dependencies.

By retaining strict visibility rules between module types, we have achieved a highly parallelized build graph. When examining the trace for a build of the full Airbnb app on a 8-core, 16-inch 2019 MacBook Pro we see full utilization of the available CPU resources for nearly 80% of the compilation phase.

Our modern build system has enabled a healthy ecosystem of command line tools which greatly simplify common tasks. Creating a module previously involved following a long checklist of error-prone steps. Now engineers create modules with an interactive Rake command. We even leveraged our Buck query interface to build a command line tool which guides engineers through the steps necessary to migrate their lib/ modules to the new module structure.

And last, but certainly not least, by no longer managing Xcode projects in source control, it is now trivial to add and remove module dependencies with an easy path to resolving any merge conflicts.

A portion of a recent PR that adds a dependency to a feature module. Starlark build files make the code easy to write and review.

Pushing the edge of the envelope together

At Airbnb, we are passionate about advancing the state of iOS development in the industry at large. We believe in the power of native applications running on mobile devices and want other companies to leverage the work that we’ve done so that they can focus more energy on building experiences that users love.

We know that we are not the only company whose application has grown organically from a small project to something larger than what the original author may have conceived to be possible. We know that many of our peer companies have undergone similar transformations to our own. Each of our aforementioned solutions are specific to our circumstances and culture, though we have seen the themes to be evergreen. We are excited to continue this work in the public domain with our peer companies as part of the Mobile Native Foundation.

Our journey of improving productivity is not done yet. As we look to the future we see a massive opportunity to use Swift static analysis to generate boilerplate code and increase code portability of features and services. We will continue to tighten the build/test/run iteration loop of iOS product development so that the weight of a mega iOS application does not stifle the joy of indie iOS development. And we yearn for a modern build system blessed by Apple.

We believe that we’ve only scratched the surface of mobile computing. We will continue to improve upon the tools, technologies, and people processes necessary to innovate at scale.

Many thanks to Francisco Díaz for advising on content and voice through multiple revisions of this article. The work described in this article is the product of many talented Airbnb iOS engineers.

If you are interested in joining us on our continuing quest to make the best iOS apps in the App Store, please see our careers page for open iOS roles.

All product names, logos, and brands are property of their respective owners. All company, product and service names used in this website are for identification purposes only. Use of these names, logos, and brands does not imply endorsement.

--

--