The Moment My Ex-Manager Was Rightfully Angry at Me : Dagger Hilt and the DI Lesson

  • Thread starter Thread starter Alparslan Selcuk Develioglu (En)
  • Start date Start date
A

Alparslan Selcuk Develioglu (En)

Guest

The Moment My Ex-Manager Was Rightfully Angry at Me : Dagger Hilt and the DI Lesson​

How did the DI decision I postponed because it was a small one cause me trouble in a project that turned into a monster with three endpoints?​

0*W4DA6whN-Kb6Kk7Q

Photo by Sadeq Mousavi on Unsplash

It was 2018. I had just started a job at one of Turkey’s largest companies operating in the hotel and restaurant sector. My start story is like a movie in itself; I’ll save that for another article. In short, these eyes saw a lot in that time… (In a positive way.)

We had two main projects:

The first was a massive project that integrated ordering, kitchen, and cash registers for restaurants. The second was a customer greeting application that my team leader initiated and later handed over to me.

At upscale restaurants, the receptionist asks:
“Do you have a reservation?”
“What kind of table would you like to sit at?”
“Have you been here before?”
“Are you going to eat, or just want to have hookah?”

We wanted a tablet app that would display a reservation list, a table list, and table occupancy status, all in the hands of staff who asked these questions. We started the project in Java. Android hadn’t yet announced Kotlin support; even when it did, the drastic transition of “Let’s rewrite all projects with Kotlin” wasn’t typically made in the industry. We were developing a tablet application based on Java 8 + RxJava + CircleCI, talking to Ruby on Rails API services.

I’m someone who forms an emotional bond with the projects I work on. I’ve occasionally looked at this project out of curiosity; they’ve abandoned the tablet app and moved to an Android phone app. I haven’t reverse-engineered it, but I’m pretty sure they rewrote it with Kotlin. “The table system was already hard to fit, so we purposely built it on a tablet. From what I can tell from the Google Play Store screenshots, they’ve completely abandoned our table plan page. With the help of my team leader, we wrote a very nice system using the intersection algorithm.” I think. I’m not unaware of the reason, but it’s their choice. Maybe they made it tablet-friendly, and the UI changes when you download it. Clever. I hope they did it that way.

Now, let’s get to the real story…
At my previous job, I was the only Android developer; I wasn’t sure how much I could improve myself. At my new job, my team leader, Erdem, was someone I greatly valued for both his technical knowledge and his character. Even though we don’t see each other very often now, I still remember him with respect. Maybe you’ll read it. Thanks again for everything, Erdem! :)

One day, he called me and explained dependency injection at length: “Instead of creating the same object over and over again, we create it in one place and use it everywhere; the code stays clean and understandable…” Then he mentioned Dagger. (Hilt didn’t exist back then; adding Dagger to the project was more of a hassle.)

“We used Dagger on a big project, and it worked out pretty well,” he said. Then he asked, “Should we add it to your project, Check&Place too?” I declined, out of ignorance, timidity, intimidation, and the thought, “It’s a small project, what’s the point?” I remember him getting a little angry at me, but I don’t remember exactly what he said — it’s been 6 or 7 years. I never imagined the project would grow at the time. In fact, he didn’t think about either.

Until…

Things changed when we sold the project to a restaurant with valet service. The restaurant said, “Don’t let both the valet and your app ask for customer’s name, surname, and phone number; scan the QR code on the card provided by the valet app and get the necessary information from there.” Bazingaaa! We had to integrate with the QR system and services of a company we didn’t even know.

Then we sold it to a hotel restaurant. This time, they said, “Anyway, a hotel guest comes in; they give their room number and get their name and surname from the hotel database.” Bazingaaa! This time, we had to integrate with a completely unfamiliar Oracle database. As I recall, it was quite a challenge for our backend team; since we couldn’t get data directly, or some data manipulation is needed something like that. They had to write a small Golang middleware that would only run on that server.

With each new connection, while the user screens didn’t change much, the backend connection layer grew larger and larger. And this wasn’t just on the backend; the application began to grow increasingly complex, with new endpoints and new configurations to connect to different services on iOS and Android. It was at this point that I said, “I wish we’d added Dagger back then.” Luckily, we have Hilt today. With multiple base URLs and services, we can clarify which client will be used where with @Qualifier annotations and inject dependencies cleanly into the project.

Qualifiers.kt:

import javax.inject.Qualifier

@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class BaseApiUrl
@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class ValeApiUrl
@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class HotelApiUrl

@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class BaseClient
@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class ValeClient
@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class HotelClient

@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class BaseRetrofit
@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class ValeRetrofit
@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class HotelRetrofit

NetworkModule.kt:

import com.example.network.BaseApi
import com.example.network.ValeApi
import com.example.network.HotelApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

// Base URLs
@Provides @Singleton @BaseApiUrl
fun provideBaseApiUrl(): String = "https://base.example.com/api/"

@Provides @Singleton @ValeApiUrl
fun provideValeApiUrl(): String = "https://vale.example.com/api/"

@Provides @Singleton @HotelApiUrl
fun provideHotelApiUrl(): String = "https://hotel.example.com/api/"

// Common logging
@Provides @Singleton
fun provideLogging(): Interceptor =
HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }

// OkHttp Clients (gerekirse farklı header/interceptor eklenir)
@Provides @Singleton @BaseClient
fun provideBaseClient(logging: Interceptor): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(logging)
// .addInterceptor { chain -> /* base headers */ chain.proceed(chain.request()) }
.build()

@Provides @Singleton @ValeClient
fun provideValeClient(logging: Interceptor): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(logging)
.build()

@Provides @Singleton @HotelClient
fun provideHotelClient(logging: Interceptor): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(logging)
.build()

// Retrofit Instances
@Provides @Singleton @BaseRetrofit
fun provideBaseRetrofit(
@BaseApiUrl baseUrl: String,
@BaseClient client: OkHttpClient
): Retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.addConverterFactory(MoshiConverterFactory.create())
.build()

@Provides @Singleton @ValeRetrofit
fun provideValeRetrofit(
@ValeApiUrl baseUrl: String,
@ValeClient client: OkHttpClient
): Retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.addConverterFactory(MoshiConverterFactory.create())
.build()

@Provides @Singleton @HotelRetrofit
fun provideHotelRetrofit(
@HotelApiUrl baseUrl: String,
@HotelClient client: OkHttpClient
): Retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.addConverterFactory(MoshiConverterFactory.create())
.build()

// API layer
@Provides @Singleton
fun provideBaseApi(@BaseRetrofit retrofit: Retrofit): BaseApi =
retrofit.create(BaseApi::class.java)

@Provides @Singleton
fun provideValeApi(@ValeRetrofit retrofit: Retrofit): ValeApi =
retrofit.create(ValeApi::class.java)

@Provides @Singleton
fun provideHotelApi(@HotelRetrofit retrofit: Retrofit): HotelApi =
retrofit.create(HotelApi::class.java)
}

API interface examples (Can be found in different folders):

import retrofit2.http.GET
import retrofit2.http.Query

interface BaseApi {
@GET("tables") suspend fun getTables(): List<TableDto>
}

interface ValeApi {
@GET("tickets/by-qr") suspend fun getByQr(@Query("qr") qr: String): ValeTicketDto
}

interface HotelApi {
@GET("guest/by-room") suspend fun getGuest(@Query("roomNumber") room: String): GuestDto
}

We choose which connection to choose in the Repository layer with enum

import com.example.network.BaseApi
import com.example.network.HotelApi
import com.example.network.ValeApi
import javax.inject.Inject
import javax.inject.Singleton

enum class Backend { BASE, VALE, HOTEL }

@Singleton
class CheckInRepository @Inject constructor(
private val baseApi: BaseApi,
private val valeApi: ValeApi,
private val hotelApi: HotelApi
) {
suspend fun fetchByQr(qr: String) = valeApi.getByQr(qr)
suspend fun fetchGuest(room: String) = hotelApi.getGuest(room)
suspend fun fetchTables() = baseApi.getTables()

suspend fun fetch(mode: Backend = Backend.BASE, arg: String): Any = when (mode) {
Backend.BASE -> baseApi.getTables()
Backend.VALE -> valeApi.getByQr(arg)
Backend.HOTEL -> hotelApi.getGuest(arg)
}
}

Of course, we can do it in an even shorter way:

@Module
@InstallIn(SingletonComponent::class)
object NetworkModuleNamed {

@Provides @Singleton @Named("url_base") fun urlBase(): String = "https://base.example.com/api/"
@Provides @Singleton @Named("url_vale") fun urlVale(): String = "https://vale.example.com/api/"
@Provides @Singleton @Named("url_hotel") fun urlHotel(): String = "https://hotel.example.com/api/"

@Provides @Singleton fun logging(): Interceptor =
HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }

@Provides @Singleton @Named("client_base")
fun clientBase(logging: Interceptor) = OkHttpClient.Builder().addInterceptor(logging).build()

@Provides @Singleton @Named("client_vale")
fun clientVale(logging: Interceptor) = OkHttpClient.Builder().addInterceptor(logging).build()

@Provides @Singleton @Named("client_hotel")
fun clientHotel(logging: Interceptor) = OkHttpClient.Builder().addInterceptor(logging).build()

@Provides @Singleton @Named("retrofit_base")
fun retrofitBase(@Named("url_base") url: String, @Named("client_base") c: OkHttpClient): Retrofit =
Retrofit.Builder().baseUrl(url).client(c).addConverterFactory(MoshiConverterFactory.create()).build()

@Provides @Singleton @Named("retrofit_vale")
fun retrofitVale(@Named("url_vale") url: String, @Named("client_vale") c: OkHttpClient): Retrofit =
Retrofit.Builder().baseUrl(url).client(c).addConverterFactory(MoshiConverterFactory.create()).build()

@Provides @Singleton @Named("retrofit_hotel")
fun retrofitHotel(@Named("url_hotel") url: String, @Named("client_hotel") c: OkHttpClient): Retrofit =
Retrofit.Builder().baseUrl(url).client(c).addConverterFactory(MoshiConverterFactory.create()).build()

@Provides @Singleton @Named("api_base")
fun apiBase(@Named("retrofit_base") r: Retrofit): BaseApi = r.create(BaseApi::class.java)

@Provides @Singleton @Named("api_vale")
fun apiVale(@Named("retrofit_vale") r: Retrofit): ValeApi = r.create(ValeApi::class.java)

@Provides @Singleton @Named("api_hotel")
fun apiHotel(@Named("retrofit_hotel") r: Retrofit): HotelApi = r.create(HotelApi::class.java)
}

Usage:

class CheckInRepository @Inject constructor(
@Named("api_base") private val baseApi: BaseApi,
@Named("api_vale") private val valeApi: ValeApi,
@Named("api_hotel") private val hotelApi: HotelApi
) { /* ... */ }

Small practical notes​

  • If Auth/Headers differ, add a separate Interceptor to each client (e.g., ValeAuthInterceptor, HotelAuthInterceptor).
  • Policies like timeout/cache/pinning may differ across three clients.
  • If you’re going to use the same interface in three backends (if the endpoint contract is identical), the cleanest way is to create three instances with three separate Retrofit.create() calls and select them with when(Backend).

Dependency Injection (DI) Isn’t Just That​


When most of us think of DI, the first thing that comes to mind is “injecting Retrofit with Hilt.” But the essence of DI is that a class doesn’t create the objects it needs internally; it injects them externally. This way:

  • Dependencies become visible: The constructor clearly shows what the class needs.
  • Testability increases: You can provide a fake/mock instead of a real service and write unit tests.
  • Configuration changes become easier: URL, timeout, cache, logger… all change from a single location.
  • Layers are separated cleanly: The UI → Use Case/Repository → Data (API/DB) chain becomes loosely coupled.

In this project (BASE/VALE/HOTEL), DI allowed us to:

  • We were able to connect the same “customer greeting” flow to different data sources (hotel DB, valet service, main API).
  • When the endpoint changed in the backend, we isolated the impact within a single module.
  • When the new integration was implemented, we added a new implementation without disrupting existing code.

The Heart of DI: “Inversion of Control” and Composition Root​


DI is the inversion of control (IoC). Objects don’t create their own dependencies; they inherit them. The decision on how these dependencies are created is centralized in a single place: the composition root.

  • Composition root: This is where the application is “built.” In Android, these are typically Application/DI modules. All wiring is done here.
  • Classes focus on pure business logic; questions like “which URL, which client, which logger?” are the job of the composition root.

Anti-Pattern: Service Locator vs. Pure DI​

  • Service Locator: The “call a global ServiceLocator.get(Api.class) and get the object” approach. This is simple, but creates hidden dependencies and is poorly tested.
  • Pure DI (Constructor Injection): “Request what you want in the constructor.” Dependencies are visible, making testing easy.

// Bad practice (Service Locator):
class CheckInController {
private val api = ServiceLocator.get<BaseApi>()
fun load() { /* api'yi kullanır */ }
}

// Good practice (Constructor Injection):
class CheckInController(private val api: BaseApi) {
fun load() { /* api'yi kullanır */ }
}

Conclusion​


The DI decision I postponed that day, thinking “it’s a small project, what’s the need?”, revealed its true colors when the project grew to three separate backends. DI isn’t just about injecting Retrofit; it’s about making dependencies visible, making testing easier, and making change cheaper.

Today, in the same scenario, with Hilt + @Qualifier, we’re cleanly separating three clients, managing the flow with a simple selection in the repository, and adding new integrations with the ease of “plugging and unplugging.” I wish I’d done it back then; thankfully, today is the day. I’ve learned my lesson, Erdem. Thank you :)

If you like this article, read more of my articles from lists please.

stat



The Moment My Ex-Manager Was Rightfully Angry at Me : Dagger Hilt and the DI Lesson was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.

Continue reading...
 


Join đť•‹đť•„đť•‹ on Telegram
Channel PREVIEW:
Back
Top