Android App Modularization at Wealthfront (Part 3)

In the previous blog post, we looked at how we started with modularization and refactored our existing code into the legacy module which was ready to be modularized into smaller infrastructure and feature modules. In this blog post, we will go in detail about how we’d refactored our infrastructure modules with the help of interfaces. 

Phase 3 – Breaking out the infra modules

The next part of the project was to break out the legacy module into smaller modules. The first partition we made was to decouple the infrastructure like api, metrics, notification, and crash reporting into their own modules. The infrastructure classes were pretty straightforward kotlin/java classes without many dependencies or resources attached to them. Whereas, features have XML resources for layouts, drawables, and strings associated with the custom views and screens. We would be tackling cyclic dependencies at the forefront and make things simpler when we would eventually refactor the features.

Breaking out the infra modules

Using Interfaces for our infrastructure

To simplify mentioning the dependencies in our feature modules, we created a module called interfaces where we would describe the interfaces of infrastructure like so

We provide the implementation through Dagger at runtime similar to the concept of portals that we saw in the previous blog post. This way all the other modules could just depend on the :infra:interfaces module to get their infra dependencies instead of having to depend on individual infra modules. This led to better dependency management and architecture, in general and we were able to move all our infra related classes out into individual modules.

Metaprogramming – Code generation

We wanted to create an easier way of creating a module as it requires a lot of ceremony like setting up the build.gradle with the proper dependencies, plugins and the appropriate configuration along with updating the settings.gradle file. This would be potentially error-prone and time-consuming. To solve this, we ended up writing a kotlin template-based code generation tool for generating java, android-infrastructure and android-feature modules. Each type of module has its own specific properties like dependencies, plugins, and files modified.

The code generator automatically generates and modifies the necessary files to create the specified type of module along with dependency injection via Dagger in the new module. Utilizing the tools that we had developed, we were able to refactor the infra modules out of the legacy module. Now the legacy module contained nothing but all our features in a single module. 

Phase 4 –  Breaking out the feature modules

The goal of this phase was to eventually remove all the features from the legacy module and build independent feature modules that can be compiled in parallel and worked on individually. We started with our largest feature which accounts for about 40% of the legacy module. This was the 80/20 of the project where removing this one module would give us a huge boost to our compilation times. Once we ripped out the large module, we had a good idea of what the process was to remove a feature module out of legacy. This included removing the screens/views, related layout files, strings from strings.xml, drawables, unit tests and then changing the Dagger component. This understanding led to us developing a tool to aid with the migration of the other modules. 

Filemover

We developed the filemover as a way of automatically collecting all the classes and files related to moving from one module to the other. This combined with the code generator helped us blaze through migrating our modules. We still needed to go back into the module to fix some compilation issues and use portals to connect the navigation. But this helped us deal with the mundane parts of migration automatically and only care about the important bits thereby saving us a lot of time.

Eventually, we were able to remove the legacy module completely and finish the modularization. We profiled the build to look at the improvements that we’ve had from modularization.

Profiling

We took a version of the app right before we started modularization and the latest version of the app to compare the build times with the Gradle’s profiling tools. We found a lot of interesting tidbits from the analysis.

  • We noticed that our codebase had grown about 33% in the 6 months that we were working on build modularization. This resulted in increased clean build times.
  • We looked at incremental build times for builds where there’s no change to the source/test code. We saw ~70% build time decrease in these instances.
  • We also looked at incremental build times for builds where there was a change to the source/test code. This was the case that applied to most of our changes in our codebase and it had improved by about ~80%.

We were able to improve the incremental compilation for builds by about ~80% and for test runs by about ~75%. This results in much faster test runs and improved feedback loop.

Conclusion

In retrospect, it was a worthwhile effort for us to take on a project as big as build modularization especially when the team and the codebase were growing at a higher pace. As a result,  we were able to realize significant improvements in developer productivity and happiness of the team. But this might not be the case for all teams and codebases. This would be very much dependent on the rate of growth of the codebase and the team along with the strategic vision of the company.  

As long as you expect the team and codebase to grow, this is a high-value project to invest in terms of cost-benefit analysis. It was one of the more challenging projects that we had tackled but investing in planning and tooling around the project helped a lot and made the transition to a multi-module repo much smoother.

I would like to thank my team for the support, amazing work and effort throughout the project. And for taking the time to proofread this series of blog posts.

Read the previous part of the series here.


Disclosure

This blog is powered by Wealthfront Corporation (formerly known as Wealthfront Inc.). The information contained in this blog is provided for general informational purposes, and should not be construed as investment advice. Any links provided to other server sites are offered as a matter of convenience and are not intended to imply that Wealthfront Corporation endorses, sponsors, promotes and/or is affiliated with the owners of or participants in those sites, or endorses any information contained on those sites, unless expressly stated otherwise.

Wealthfront Corporation may from time to time publish content in this blog and/or on this site that has been created by affiliated or unaffiliated contributors. These contributors may include Wealthfront employees, other financial advisors, third-party authors who are paid a fee by Wealthfront, or other parties. Unless otherwise noted, the content of such posts does not necessarily represent the actual views or opinions of Wealthfront or any of its officers, directors, or employees. The opinions expressed by guest bloggers and/or blog interviewees are strictly their own and do not necessarily represent those of Wealthfront Corporation.

© 2020 Wealthfront Corporation. All rights reserved.