An honest look at migrating an Android app to a modular architecture (Part 1)
This article is the first chapter of a multipart series presenting the study of modularizing an existent large-scale Android application, highlighting the challenges and tradeoffs of a certain approach above the other.
At PagSeguro we are on the verge of migrating to a modular architecture on our 4 years old PagBank app. The app was launched on December 2016 and it was conceived to be a simple digital wallet with limited functionalities. It was quite an experimental product and a chance to increase the company’s portfolio at that time, but throughout its journey it went from a simple prototype to a product used by millions.
The first version was built by just two teams, with roughly 7 people each, and time to market was a key strategy to the company, who wanted to launch first and then iterate to achieve a product market fit. As the ambition for this project was very limited at its inception, architecture, patterns and decisions towards scalability were not a big focus.
As the app grew the company followed along, scaling the business and the number of teams developing the app. This brought a variety of challenges as coordination, standardization and quality. Soon enough the app’s architecture was compromising the scalability and productivity of several teams who need to deliver an increasing number of features.
The Ageing Hero
The app was structure on a single module with features divided by package, which gives quite good code organization, but lacked the means to enforce decoupling of flows. It was also designed to have a single activity and every flow would be implemented using a stack of fragments coordinated by this master activity. We have to remember that back then the Navigation Component (Android Jetpack) did not yet exist, so the abstraction to facilitate the navigation between fragments and its coordination through the main activity were all done by “hand”. Singletons were also used widely throughout the app to ease the use of common abstractions, but without an aim around testability and separation of concerns.
This strategy is fine for small apps, as it reduces architecture complexity, but takes its toll fast on large applications. The main problems we wanted to address were:
- Loose coupling: We want to enforce our teams independence, allowing them to develop and deploy their features without having to care about the implementation of other parts of the app.
- Testability: we want to have a great test coverage to give confidence for developers deploying new functionalities that nothing will break the app.
- Code reusability: to enforce that base structures are created in a way that could be reused in other parts of the app.
- Build and delivery performance: as the app gets bigger, so was the build time and CI duration. We want to reduce that and be able to enforce team’s independence on delivering new functionalities to our users.
Modularization came as one of the initiatives that could help us deal with those problems. Each feature could have its own independent module and be completely decoupled from the rest of the app, that address testability and team independence. Base structures could be encapsulated on separate modules as well and be injected as dependencies where required, addressing code reusability. And finally, build and CI times could benefit from caching by building and testing only modules that has been modified on that branch.
As modularization looks as the right fit for bringing the app to its new era, we are researching different approaches, trying to find one direction that will meet our requirements. First, let’s list what those requirements are:
- Modularization by feature.
- Multiple activities architecture.
- A solid navigation structure: The structure must be easy to extend and maintain.
- Easy module setup: The creation of new modules and its integration with the app must be as effortless as possible.
- Be prepared for Dynamic Delivery. It is not a requirement for this phase, but the proposed solution must have an eye for use in Dynamic Features in a way that no re-structure of the base architecture will have to be made when this feature is adopted.
- Don’t rely on reflection. The project uses tools that obfuscate the production code and we don’t want that the usability of the modular structure depend on having to add custom rules to avoid obfuscation.
- Feature to feature navigation.
Having those requirements established, we had an internal look into our current scenario and raised the following points with potential to affect the solution we are seeking:
- All features are still on the :app module.
- Features are currently divided by package.
- Multiple activities approach with every entry point of a feature being an activity.
- Some base abstractions were already moved away from the :app, such as: Part of security, network, persistence, tracking/monitoring and other common utilities.
- There are still some base structures highly coupled on the :app module.
- Some features directly depend on abstractions of other features.
- It is possible to navigate between features.
On the next part we will present the first step we’ve took on our adventure of breaking down the app into various pieces and learn that what it seems to be straight forward would be more treacherous than we anticipated.