Around December 2018, we decided to turn the iFood’s code base into a monorepo. By then, we had one target (the consumer app), and two main dependencies on different repositories. We started developing a new product inside the app, but for productivity reasons, we decided to create a new repository.
TDLR; it was hell. Different versions of the dependencies conflicting, tons and tons of duplicated code, things being developed in parallel without need. 😩
It took some effort, but we organized our codebase, split them into subjects, brought the new product and those two dependencies to the main repo. At first, we used development pods, but later we started using Buck.
Which building tool and how we manage the monorepo is not the focus today, the focus is to talk about how we split and organize our modules and why I like them so much.
Since the beginning, we decided to follow the concept of microfeatures. Just like microservices or the new hype of microfrontends, microfeatures split functionalities into small and concise modules.
For us, the high point of the definition was the detachment of what is a “framework/sdk” and what is a functionality for the final user. Those are named foundation µFeatures and product µFeatures, respectively.
The main definition we got inspiration from is this one by Pedro Piñera, the author of Tuist, a build tool to manage Xcode projects
For us, foundation µFeatures are those modules responsible for a specific task, such as HTTP calls, or UI components, error logging, event monitoring, remote configuration workers, and so on.
They don’t have any business logic involved, they expose a set of classes or protocols and we can import and use them freely throughout the app. They can be fully tested and compiling time is ridiculously slow.
Disclaimer: we have a dependency management structure in the main app to centralize these “workers” and we avoid importing other foundation µFeatures inside each other to ditch dependency cycles.
On the other hand, product µFeatures are those functionalities that your user can see. For example, a login flow or a checkout page, a restaurant’s menu, etcetera.
They not only have business logic, but also have the screens, the routers, use cases, repositories, and all those Clean Swift and Clean Architecture layers.
It is very important that one product microfeature does not import another product microfeature, cause dependency cycles here can get really wrong, really easily.
We are evolving in having routes to deal with controlling the coordination between those modules. We use this framework.
Each one of these modules can use the architecture that fits its needs, but as a convention, we use either VIP/Clean Swift or a more robust approach with Clean Architecture. That’s content for another post.
You’ve probably got the hang of it by now, so onto the advantages and my opinion on why this setup works well for us.
To exemplify all the following points, imagine you have a shared UI component and you need to change its design. It’s a very common text field, so basically all your product µFeatures use it.
One pull request, all the changes
If the component in question was in a separate repository, you would have to apply the change to it, then go to the product µFeature’s repo one by one and update them, creating one pull request each. They might not be approved concurrently, so your released app might have inconsistent layouts between the screen flows.
Only one version in production
As a consequence of the situation above, in a multirepo scenario, you could have multiple versions in the same production app. Different behaviours, layouts, bugs. 🙃
Ease on sharing code
Our squads work in different pieces of the product, but we do share tons of code. From visual components to features such as biometry, encryption, automatic request retry (the list can go on and on).
Testability and maintainability
It’s much easier to test and maintain a small, well-organized set of code that has one single responsibility. It also allows for “plug and play”, in case you wanna do some A/B testing, or try different libraries. Moreover, it clears out the weaknesses and points of attention when coming to code coverage measurements.
Faster build times
That’s the number one reason for most articles about monorepos I read. And it’s true, we have decreased build times from fifteen minutes to few seconds per module. Most of my work focus on a specific module and I don’t need to run the whole app every time.
Each microfeature has an example app. They have entry points, with fixed scenarios that cover those feature’s capabilities/screen flows/functionalities. When I’m done with tests and development of a module, I integrate it with the main target and only then I run the whole app. Yeah, that still takes a while, but I do that way less than running the example apps separately.
I’m a huge fan of monorepos and I strongly identify with the foundation/product microfeature terminology explained above. It suits our project, it boosts our productivity, it scales well and the downsides haven’t overcome the advantages so far.
However, I don’t think it is adequate for every single project. Nothing is a silver bullet. Thus, analyze and study different options and discuss with your team before applying any of them.