Over the past year, the Instabug BugSquad has grown rapidly. With a 200% increase in team size, we had to think hard about how to scale an engineering company without added bureaucracy and with improved productivity. In the end, this meant we had to refactor our code base.
As with all complex engineering endeavors, things tend to move at a slower, less efficient pace when teams working on them grow in size. To address this problem early on, we’ve reorganized the engineering team into small, autonomous cross-functional teams. We call those teams Squads. Each Squad owns a product that Instabug offers. More on that in an upcoming blog post.
To give each Squad the agility it needs to move fast, without being interdependent on other Squads, we’ve decided to split our large monolithic codebases into smaller ones. On the iOS side, this meant we had to make radical changes to the way we structure our code and how we build and distribute our SDK. In this blog post, we’ll be discussing the details of the changes our iOS team has made.
The biggest challenge of our old setup was that we have multiple cross-functional Squads working on different products, but we have a single code repository for our iOS SDK that we distribute as a single framework. We quickly realized that we would be facing the following challenges.
- Features and changes that touch the core functionality of the SDK would be hard to plan and keep in sync across different teams.
- Having engineers from different Squads contribute to the same code base would result in duplicated effort.
- It would be hard to give each Squad the flexibility to plan their releases and keep their release process streamlined. We want to eliminate dependencies between different Squads, but maintaining a single framework means that different Squads are still dependent on each other.
- It’s going to become significantly more challenging to maintain our goal of providing a lightweight, high-quality SDK.
Refactoring Our Code Base
Over the past few months, we took a deep dive into a project to refactor our code base and our framework into multiple different ones.
Our goals were to solve the challenges above, in addition to increasing modularity of our code and encouraging reuse across different Squads.
Here are the steps we went through to refactor our code base.
We started by identifying the different components in our SDK. A component represents a product that’s owned by a Squad, and would end up being distributed as a separate framework. We identified four different components: Bug Reporting, Crash Reporting, Surveys, Chats, in addition to a fifth component, Core, which is the foundation of the SDK and encapsulates common functionality shared between different frameworks.
Deciding What Goes Where
With the frameworks listed above, it was very easy to end up cramming almost all our code we have into Core, and limiting other frameworks to only have UI related to the products they serve. We made a conscious decision from the beginning to keep Core as lightweight as possible.
Having decided that Core should be lightweight, that meant it could not contain any business logic related to any of our products. Core had to only contain abstract classes that serve things like networking, persistence, logging, etc, while other frameworks would contain the business logic, which uses components offered by Core.
We wanted to maintain complete separation between the different frameworks we created. We relied on the local pods feature offered by CocoaPods, which allows us to create dependencies between our frameworks. CocoaPods is not integrated into our build process to product the final framework we distribute.
Designing Cross-Framework Communication
Since our final SDK is now made up of several frameworks, we had to design and maintain public APIs between those frameworks to make sure communication between them is clearly defined. Those APIs serve as a contacts between different Squads.
To make sure we can maintain compatibility between the different frameworks that make up our SDK, we started versioning each framework separately. So for example, Instabug 8.0 depends on Core 1.5 and Bug Reporting 1.0. This also gives each Squad the flexibility to release versions of their framework whenever they are ready.
How We Build Everything Together
If you download Instabug right now, you’ll see that it’s only made up of two separate frameworks: Instabug and Instabug Core. While we maintain different frameworks locally, we still distribute all our products as one framework that depends on Instabug Core. We’re only doing this temporarily until all Squads are ready to roll out their frameworks.
To be able to build and distribute our combined framework, we have a target in Xcode that contains all other frameworks in its Link Binary With Libraries build phase. We then use a shell script that builds this target generating a binary for devices and a binary for simulators, then lipos them together. The script then does the same thing for Instabug Core to produce the final results of two frameworks.
This is how our setup looks right now in Xcode:
|- Instabug.xcodeproj |-- Targets |--- Bug Reporting |--- Crash Reporting |--- Chats |--- Surveys |--- Instabug (results in our combined framework) |---- Link Binary With Libraries |----- Bug Reporting |----- Crash Reporting |----- Chats |----- Surveys |- Pods.xcodeproj |-- Instabug Core
Relying on Build Scripts
If you’re wondering why we cannot build our frameworks directly using Xcode, the reason is that as of version 9.2, Xcode still doesn’t support exporting a fat binary that contains architectures for physical iOS devices (armv7 and arm64) in addition to simulators (i386 and x86_64). So building a framework has to be done through a script that builds each group of architectures separately, then uses lipo tool to generate one fat binary that contains all architectures.
To make sure we haven’t broken anything with all this refactoring, we relied heavily on unit testing, UI testing, as well as manual QA testing for scenarios that cannot be automated.
Refactoring our SDK has been a massive endeavor involving months of working late nights and weekends, making tough decisions, and consuming endless cups of coffee. But we know that what we accomplished this year will impact Instabug positively over the long term. We now have a battle-tested team, a more agile company, and, ultimately, a stronger product for our users.
With our new setup, we’re able to move much faster and maintain complete autonomy of each Squad. The result of which is that we have a lot of exciting things in our pipeline that we’re super excited to share with you soon!