1 app, 18 products — A journey from monolith to a microapps codebase
By Vikas Maini
GO-JEK. “An Ojek for Every Need“.
Like literally, EVERY need.
From food, transport, massage, cleaning, payment, shopping, logistics, delivery, medicine, entertainment, you name it, we have it.
Oh, and in case you’re wondering, an “ojek” is a motorcycle taxi, or cart bike. It’s a licensed form of transport in some countries.
We have around 18 services up and running across our platforms and more are coming. The amount of services GO-JEK helps you with is nothing short of phenomenal.
This is the story of how we built our iOS app:
The Beginning
Our iOS codebase was Monolithic. By nature of our vastness, a lot of code was jumbled across products. The code was so convoluted, no one knew how a small change in their code would affect the others.
Inevitably, there was a ripple effect; solving a bug would raise a new one. Debugging was not easy. It took almost 45 minutes to build, whether we were building from scratch or just changing a single line of code.
The bizarre amount of time it took to test the smallest changes was proving to be cumbersome. This time wasted was worse on our Continuous Integration pipeline. And it was fairly obvious because we were handling nearly 18 products inside a single repository, and separate teams were simultaneously working on the same codebase. So much so, we used to have a team whose only responsibility was to fix bugs for the entire app.
Every new iOS developer at GO-JEK spent a few weeks with this team, fixing bugs and learning the codebase, before being assigned to a different product team. If we wanted to improve the codebase, we first need to figure out how to spend less time on bugs.
The VIPER Phase
We needed to reinvent ourself. After analysing our main issues, we decided to remove all cross-product dependencies and restructure our products using VIPER.
We decided to build GO-PAY and GO-POINTS, with this new structure. These products would lay a foundation for future products and become a template for refactoring existing ones. It was important for us to choose the architecture best suited to handle these many products together. Initially we chose VIPER, and here’s why:
Pros:
- Code becomes much more testable
- Separation of components really well
- Very high encapsulation
- Easy removal of any feature
Cons:
- Two-way communication between modules is not possible
- Difficult to step through while debugging since everything is abstracted behind a protocol
Irrespective of the cons, we accepted this architecture and continued the transition for other products as well. But to start this for existing products, we needed a firebreak.
Firebreak — Modularisation
An obstacle to the spread of fire
We pinned down the architecture and got the implementation right in products. But, we needed to modularise the whole approach. We were still counting 45 mins on average of compile time. This firebreak exercise was going to literally stop feature development for a few weeks and let us just focus on modularisation tasks. It was a hard decision at the time, but to get this right, we had to get things right internally. Features, and product development comes only after practicing what we preach — adhering to the principles of good software development practices.
“It’s not about being the first to market, it’s about being the first to get it right.” — Benny Xian
This one step did wonders for us in iOS. On Mobile Consumer App, our main goal for Firebreak, was to break down the single repo into multiple product repos as a part of modularisation task.
Every iOS developer or a small team became POCs for each product. We identified each product’s files, assets, fonts, resources and moved them out to separate repositories. Sometimes we had to make duplicate files in separate repos as both products were using them. But we were ready to hold this duplicity for now. Home, Settings, Help, History, Tabs, Navigation stack, Analytics were still at the core of GO-JEK holding 18 products through CocoaPods.
Advantages of breaking down the repos gave us the following benefits:
- Pushes for platformization of the Consumer App. All essential services to be extracted out in platform library. This includes Network, Notification, Remote Config, Analytics, UI Components,
- Gives more independence to each product on moving quickly without being affected by other product changes
- Easier to track quality and impact by a product on the integrated app. E.g. app size, code quality can be tracked at product repo level.
- Easier rollback of product specific changes
- Enforces coming up with better API contracts/generalised solutions, because of module separation.
Some of the retrospective points we collected from our team:
- Satisfied with Firebreak
- Compile time down on iOS
- Many tech clean up tasks pending, need more time on Firebreak/Tech clean up activities
- [Action] have a small team continue on pending critical tasks
- Build process with pods is getting complex
Clearly, developers were happy with the progress and they don’t have to wait for the main codebase to compile and test the product. Now, developers host apps to run and test their products in complete isolation.
This gave tremendous freedom fo the teams to kick start feature development in full throttle.
But we were not done yet. The main GO-JEK app was still taking about 40 mins on average to compile all product’s pods and the main code. We can’t get the leverage to have another firebreak to fix more things, so we continued with it. But we knew the problem was at core.
Launchpad
Our main goal for the iOS app was to boost developer productivity. All the measures we took in this endeavour helped us reduce the time taken to build, test and ship out new features and ensured quick bug fixes.
The main motivation behind separating product codebases was to make product teams independent of each other. This also resulted in immensely reducing the time taken to build individual products. Since each team was no longer dependant on other products, we were able to ship independently.
During this time, we introduced the concept of dummy launcher apps. These were single screen apps which sort of had an index of a product’s various screens. Every product repository had its own dummy app and product developers used these apps to test the features they were building.
There were few drawbacks to this approach:
- You cannot test a product in complete isolation from other products in the real GO-JEK environment
- Testing and debugging analytics events were not possible
- Testing and debugging remote notifications and deeplinks were not possible
- Testing a complete booking flow — from placing an order to viewing the history were not possible
- Testing logout/login events were not possible
This meant in order to fully test a feature, developers had to build the actual GO-JEK app (with all products) with their product code in it. And debugging and fixing bugs required multiple app builds. Preparing production releases required a synchronised effort from all teams since it was complicated to automatically merge many codebases together in a reliable way.
To fix these, we came up with Launchpad. It was a 6-week project where we re-wrote the GO-JEK app (barring actual product code).
The main vision and goals behind Launchpad were as follows:
- Sandbox all product codebases into their own repositories
- Make GO-JEK fully independent of products
- Wrap the GO-JEK app into a framework that can be imported into products
- Allow products to be tested in isolation in the real GO-JEK environment
- Come up with unified APIs to integrate a product into GO-JEK
- Allow for easy removal of products from GO-JEK
- Allow a product or a group of products to be submitted as a stand-alone app to the App Store without major code changes
- Allow products to be individually profiled and audited for quality and performance
- Export APIs, allowing products to show dynamic components in the home screen without modifying the GO-JEK codebase
A clean build succeeded in just 8 minutes. We’re actively improving this further with compiler optimisation tweaks.
Throughout the past year or so we’ve seen the codebase transforming radically and maturing gradually. This helped us shift our focus to build some cool features which improves the overall experience of the app. To sum up the post, as John Johnson famously said,
“First, solve the problem. Then, write the code”
And yes We’re hiring! For any roles in Engineering, Design or Product Management, or anything at all, visit gojek.jobs. Come solve amazing engineering challenges.
Happy hacking :)