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.

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 that will be presented modally or installed into a . A feature module should be scoped to a single user-facing destination when possible, though it may contain multiple s 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 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.

Source link