Dagger and Multi-module Traveloka Android App

Alifa Nurani Putri
Traveloka Engineering Blog
11 min readSep 9, 2019

Authors: Alifa Nurani Putri (Android Software Engineer), Yusuf Cahyo Nugroho (Android Software Engineer)

Dagger is a popular dependency injection framework for Android development. If you’ve heard about it and want to start immediately implementing it, there are plenty of articles out there that will help you to do that. Instead, what we want to share with you today is our journey at Traveloka in implementing Dagger while our Android app continues to scale up, specifically in facing the adoption, problems, and refactors along the way which hopefully, you will be able to pick up invaluable lessons from.

Before we start, please be advised that this article focuses mainly on Dagger implementation for a large-scale multi-module app (Traveloka app is now delivering 25+ products). It’s best suited for those who have had basic understanding of Android development, dependency injection, and Dagger 2 (Component, Subcomponent, Module, Scope).

SPOILER: From our journey, you can get tips to decrease build time and cold-start time, make a clearer dependency, and get the feasibility of Dynamic Feature Module.

In the Beginning

It was the end of 2016 when we decided to adopt Dagger. At that time, Traveloka app was quite stable with millions of users already using it and we didn’t want to take the risk of destabilizing it, despite having the knowledge of how powerful Dagger is. So, we began using Dagger only in Application class (TravelokaApplication).

We created a single Dagger component named AppComponent to hold Context, Application, and providers (in our architecture, provider is the layer responsible for reading / writing data to be used by the presenter). The block of code below describes how simple it is to start our Dagger adoption.

A few months into 2017, Traveloka has launched a number of new products. At that time, we have proved that our Dagger implementation in the Application class (TravelokaApplication) didn’t cause any problem. We then started implementing Dagger as well into product’s codes as Subcomponent of AppComponent. Each product also has its own provider module, as shown by the diagram below.

First Dagger Implementation for Multi-module App at Traveloka

Our starting adoption of Dagger ran smoothly. It was nice to know you, Dagger.

We Started Facing Problems

All is well until we realized that our single Dagger component — AppComponentcame to be a God Object. It had 82 Dagger Modules and 36 Subcomponents. Moreover, the generated file DaggerAppComponent had 17705 lines of code.

Back in the days, when we decided to use a single Dagger component, we had predicted that it could get sticky in the future when the app keeps scaling up. But, we had another reason at that time to mute our prediction. In several cases, we needed singleton for holding state across activities. Yeah, please don’t judge us :D. We knew it was a bad practice but didn’t think too much about the drawbacks in advance.

Without further ado, here are the top 3 problems we faced;

1. AppComponent Holds Excessive Objects

Using a single Dagger component (AppComponent) means all injected objects will still be kept alive as long as the application is alive whether the object is still being used or not. To give you a picture, let’s say a user opens the Traveloka app and does several actions: 1) Login 2) Search for a flight 3) Search for a hotel 4) Search for a train 5) Make a train booking 6) Go to the payment page. Objects that are injected in Flight, Hotel, and Train search pages are still kept alive until the 6th action, even though they’re not used anymore.

Another drawback is cold-start time. Dagger will create DoubleCheck object for each object that belongs to the component (AppComponent) on initialization (application OnCreate). The more objects contained in AppComponent, the more time is needed to initialize Dagger, which will in turn, cause a longer cold-start time.

2. Diamond-shaped Dependency and Build Time Issue

Simplified Diamond-Shaped Dependency at Traveloka

Diamond-shaped Dependency describes the module dependency in the Traveloka app. Application module (app) depends on all product modules and each product module depends on core. To clarify, each product module may contain more than 1 Android library module, such as the no-op module. Other kind of modules will be introduced later in this article.

This diamond-shaped dependency causes a build time problem in incremental build case. Whenever public Java-based code changes in any product module, the app also needs to be rebuilt, which may take additional 2+ minutes.

So, what is the relationship of diamond-shaped dependency with Dagger?
AppComponent, our single Dagger component that holds all of the dependencies, can only be located in the app for it knows — has reference to — everything. By continuously using a single Dagger component, we keep that diamond-shaped dependency alive and won’t be able to solve the incremental build time issue.

Breaking diamond-shaped will increase build time

3. Dis-utilization of the Upcoming Dynamic Feature Module

Dynamic Feature Module (DFM) requires a feature module (the same definition as product module in Traveloka case), which depends on the application module, not vice versa. By maintaining the diamond-shaped dependency, we won’t be able to try Dynamic Feature Module.

The further we are in this journey, we started to believe that a good library with miscalculated adoption will eventually produce drawbacks that could dwarf its benefits.

The Refactor Journey

Finding the best Dagger implementation is not an easy refactor story for us. Let me take you through the journey step-by-step. It might be a bit long. Therefore, my suggestion is grab a cup of coffee or tea and let’s enjoy the journey together. :)

First things first, we should agree that this refactor aims to make our AppComponent smaller. By doing this refactor, we expect a faster build time, a lower cold-start time, and open the feasibility to implement Dynamic Feature Module.

Convert Subcomponent into Component

I’m sure that some of you are questioning, “Why does it sound so complicated? Why not just convert each product’s Subcomponent into independent Component?” Our answer, “Nice, you get the point!

Condition after converting Subcomponent into Component

We agree on the idea of independent and internal Dagger component, where it only holds objects that are used by its own product module. Product component (e.g. FlightComponent, HotelComponent) can live in screen levels like Activity and Fragment. You can also use Scope, as we use @ScreenScope to annotate this case.

Use Scope as needed. Otherwise, object will always be recreated when it’s needed.

Unfortunately, in Traveloka case, implementing modularization is not easy because of Cross-Sell Modularization. To give you an example, Traveloka offers cross-selling in Flight together with Hotel and Car Rental that can make Hotel and Car Rental views to appear in the Flight screen.

If we only convert Subcomponent to Component, then Flight will depend on Hotel and Car Rental for cross-selling. But we want to avoid that because we are actually deviating away from modularization; a concept of breaking system into multiple degrees of interdependence and cross-independence. Flight shouldn’t know about Hotel implementation (and vice versa), but should only be encapsulated with a contract. By defining such dependency, it could also potentially produce circular dependency whereby, for example, Hotel needs to cross-sell Flight on its page and Flight in turns needs to cross-sell Hotel on its page.

Don’t make a product module depends on another product module. Each has separate business model and the dependency should be encapsulated with contract.

Cross-Selling Example

Core-Layer Module to the Rescue?

There was an article, which suggested creating a core-layer module to place shared objects as a contract. We have used this solution and named it all-api module. In Traveloka case, almost all product modules need to depend on all-api. But, this solution could be problematic as well when all-api is overloaded for It will cause a longer build time and make the modularization unclear.

Introducing Product API Module

Our next attempt was to separate all-api into individual product api module. A product module should only depend on some specific product api modules as needed.

Therefore, we created a Dagger component for each api module. It will be the responsibility of an api component to hold the dependencies needed by other product modules. To piggyback on the previous Flight, Hotel, and Car Rental example, the diagram below describes the dependency pattern of using product api.

Using of Product api Module

For example, a service named HotelSearchService will be used by Flight to search for Hotel inside Flight’s cross-selling screen. We need to create an interface for HotelSearchService. Then, HotelApiComponent (Dagger component for hotel-api) will be used to expose HotelSearchService. Flight only needs to depend on hotel-api and HotelSearchService can then be injected.

Problem: hotel-api module only has the interface of HotelSearchService. The implementation is still located in hotel module. How to bind that interface to its implementation?

Problem in api Module

Solution: Since only the app module knows about everything, it should be the one responsible to do the binding task during the app’s initialization. It will trigger every product module to register the implementation to the core module. Back to the case, by the time flight module needs HotelSearchService, the implementation is already bound and ready to be used.

Introducing Product Base Module

Another problem: some objects need to be present immediately after the application has finished with initialization.
Solution: We created another type of module named base. Those immediately-needed objects will be held by product base Subcomponent.

For example, we use it for our deeplink service where, every module has its own deeplink service that should be present immediately and must be alive as long as the application is alive. We need to register deeplink services to a collection (e.g. Set, Map) using Multibindings during app initialization. Product base module (a Dagger module) is responsible to bind that. The collection itself will be held by AppComponent.

Challenge: At Traveloka, every product team has individual priorities. The central Android team can’t enforce all product engineers to refactor at the same time. Therefore, base product module is created to temporarily handle this challenge by resolving dependencies from product modules that haven’t been refactored yet (still don’t have api component).

For example, the graph below shows an example where FlightProvider needs PaymentService. But, in this case, payment module hasn’t been refactored. This is solved by creating FlightBaseSubComponent as a Subcomponent of AppComponent, so it can expose PaymentService.

Example Usage of Base Module

Bottleneck and dagger-bridge Introduction

I have mentioned before that we wanted to break the diamond-shaped dependency for it will make a faster build time and open the feasibility of adopting Dynamic Feature Module (DFM). So we did. app module only needs to depend on product api and product base module if needed. We can decouple product module from the app module and try convert product module as a feature module of DFM.

Problem: We faced DFM issue that cause a bigger application size when implementing DFM. Consequently, we decided to postpone the adoption of DFM.

Then, is it possible to at least make the build time faster for now?

Dagger Bridge Layer

Solution: We introduced a new layer module called dagger-bridge, which is actually an empty module. What we want to achieve is to pseudo-decouple product module from app module. Currently, only dagger-bridge that needs to be rebuilt when any code is changed in the product module (incremental build).

The Idea in Brief

In a simple way, every product module could have 3 kinds of modules: Internal, api, and base. Internal is mandatory (the main module), api will be used when other module needs dependency from it, and base will be used when an object should be present immediately after app initialization. The diagram below illustrates the relationship among those modules followed by the associated code examples afterwards that explain our Dagger refactor implementation.

Final Dagger Refactoring Concept

Internal Module

Flight component is located in flight module and annotated with @ScreenScope. It depends on HotelApiComponent and FlightBaseSubComponent.

Api Module Explanation

HotelApiComponent contract is located in hotel-api module, which is used to expose HotelSearchService needed by flight for cross-selling .The implementation of HotelApiComponent is located in hotel module, which is used to bind interface to implementation.

Base Module Explanation

FlightBaseSubComponent is located in flight-base module, which is used to expose PaymentService from AppComponent (in this example, payment module hasn’t been refactored)

In addition, FlightBaseModule, located in flight-base module, is used to provide objects from flight module that are needed immediately after application initialization.

Proof

What do you think about our new Dagger implementation? Will you believe if it does make a better impact? Let our benchmark’s results speak for themselves.

Initialize Time Comparison

For a sample case, we refactored 1 out of 25+ product modules. We proved that the time needed to initialize dagger had decreased from 357ms to 219ms on average from several experiments.

Incremental Build Time Comparison

This scenario was used to verify that the average incremental build time did decrement after the refactor (with the use of dagger-bridge). We compared the build time by changing a public method signature before and after refactoring. We also used two product modules for the sample.

Build Time Before-After Refactor

We ran each build in our local machine (MBP 2017 15" i7 16GB 500GB SSD) and used Gradle Enterprise to analyze. In short, using dagger-bridge module will negate several tasks from being executed if the changes were only present in the product module. Those several tasks were :app:processDebugAnnotationsWithJavac and :app:compileDebugJavaWithJavac.

What’s Next?

Currently, we are still midway through the refactoring process. As you are reading this article, 9 out of 25+ product modules in Traveloka app have already completed the refactor.

After all product modules have been refactored, we want to prove that our new Dagger implementation is Dynamic Feature Module-friendly. Bringing a product module to the lowermost part of the dependency graph (depends on app, not vice versa) will hypothetically make another decrement in build time. Hopefully, we can prove it in due time and share the story with you again next time :)

Anyway, this experience is best for our case and therefore, It could be different on your end. Share your potentially different experience or better approach(es) with us with your comments below.

--

--