Why and How We Moved Our iOS Code to a Monorepo

Gio Lodi
iOS Platform Lead

About a month ago the iflix iOS team consolidated all the internal dependencies previously living in their own repositories into a single one, which we refer to as “the monorepo”.

This post shares what brought us to make this transition, how our monorepo is setup, and how it’s behaving for us.

Edit of picture by Stephen Radford

Before the monorepo

Our iOS app is doing a number of things. To maintain a bit of sanity and tame its complexity we tried to write isolated components taking care of only one thing.

Splitting software into distinct components responsible for an isolated subset of the functionality is a common form of separation of concerns, and as long as the boundaries are drawn correctly, it’s a great practice. So we ended up with iflixKit, the framework that interfaces with all our backend services, LicenseKit, the framework that takes care of DRM validation, and NetworkingKit, a small framework with a bunch of networking related functionality that both iflixKit and LicenseKit rely upon. These frameworks are the internal dependencies of the iOS app.

Each of these four components was living in its own repository. iflixKit and LicenseKit were fetched by the app in the form of git submodules, while NetworkingKit using Carthage.

This setup delivered well on the promise of keeping our concerns separated, and allowing different team members to work on different parts of the application without stepping on each other’s toes, but it also introduced considerable friction.

The iflix app is far from being “done”, a few of the backend services are still in flux, and we’re working hard to replace web views with native components. More often than not we found ourselves having to update one of the frameworks and the app as part of the same feature, usually with some back and forth.

The process would more or less look like this:

  • A PR is opened on the framework repository.
  • A PR is opened on the app repository, its the first commit makes the framework submodule point to the branch of the corresponding PR.
  • The framework PR is code reviewed, usually resulting in the author updating it thanks to the feedback of the team.
  • The app PR is code reviewed, again this usually result in some updates to it. If the framework pull request went through updates as well a new commit needs to be made so that the submodule can get them.
  • The framework PR is merged on master.
  • A final commit is made on the app PR to point the framework submodule to the just update master branch.
  • The app PR is merged.
  • Rinse and repeat.

This process has a number of moving parts and back and forth, and it is error prone.

Sometimes only one of the two PRs was reviewed, it was easy to forget to update the app PR to point to the latest master of the framework. And let’s not talk about the issues that could, and would, arise when different PRs where opened on the same framework at the same time.

What we realized is that yes, the separation of concerns is great and helps us, but the speed at which each component changed was such that the time we spent updating submodules or Carthage dependencies was considerable.

So we asked ourselves: “can we have the same separation of concerns without the friction of updating framework’s references?”.

The transition to monorepo

As said, what we like is having multiple frameworks, what we don’t like is having to continuously update the pointers to their repositories.

How can we avoid having to do that?

A possible solution is to bring everything into the same repository. Xcode projects support having multiple targets inside them, some of them application, some of them tests, some of them frameworks, so we don’t have to give away our beloved frameworks.

And this is how we configured our monorepo, as simple as that.

One day, right after an App Store release, we went into a war room, merged all the outstanding PRs, and moved the source code of each of our three internal dependencies into the app repository, creating a new framework and unit tests target in the project for each of them.

The process took us a couple of hours, plus an extra half a day to reconfigure the CI automation under this new setup.

How is this working out for us

The expectations on which we based this experiment were that we would have saved time by not having to jump across branches, review PRs spread across repositories, and having to frequently update git submodules.

Those expectations have been greatly fulfilled, and we’re glad we made the transition.

We don’t have any empirical data on the speed at which PRs are opened, reviewed and merged, mainly because every PR is different from the others. The feeling the whole team has is that yes, we are moving a bit faster, and we don’t have to use time in the nuisance of updating submodules.

Tradeoffs

As with every decision in software development, there are some tradeoffs that we made when moving to the monorepo.

The main issue we have is that now builds are slower, due to the bigger amount of code that Xcode has to compile. Our codebase is mainly Swift, which doesn’t help with that.

The builds are even slower on our CI, as we decided to always run all the tests for all the targets, even if only one of them changed.

The reason we made this choice is because we value the confidence of a full unit tests run more than the time it takes to run the build. We also have three boxes on which the CI runs, so slower builds are less of a problem.

The transition to monorepo introduced a new unexpected little challenge as well. Now that all the code lives under the same project we need to pay extra attention when looking at pull requests that add files to make sure that each new file belongs to the right target. This is not straightforward with a single project file.

Speeding up the build times and making where a file belongs clearer is what we’re going to focus next, so stay tuned.

“Is monorepo right for my team?”

The intent of this post was not to sell you on the monorepo approach, nor to say that this setup is better than other ones. I hope that sharing our experience has given you more information to make a choice for your team.

What I can say is that if you are working on a project spread across different components which are changing at a rapid pace, and you are noticing that the number of commits you make that are only updating internal dependencies is starting to grow, then you should consider consolidating those dependencies in a monorepo.

If you have any feedback or comment on this setup, or if you’d like to bounce ideas on how to move to a monorepo, or improve an existing one fell free to get in touch with us on Twitter at @iflixdevs.