We often encounter projects where Dagger 2 is added midway through development. The result is a tangled graph, memory leaks, and bugs that cannot be reproduced locally. Dagger generates code at compile time — no reflection, but this safety comes at the cost of architectural discipline. Our experience: setup from scratch takes 3–5 days, and refactoring a chaotic implementation takes from a week. With over 5 years of work, we have set up Dagger on 30+ projects of various scales — from startups to enterprise applications. We guarantee clean DI code without runtime surprises. Get a consultation for your project: order an analysis of your current architecture.
How to properly design a component graph?
The typical schema for a large app: AppComponent (Singleton) → ActivityComponent (PerActivity) → FragmentComponent (PerFragment). Each level is a subcomponent or dependent component. The architectural mistake is placing all dependencies in AppComponent, which increases build times and complicates testing.
@Singleton @Component(modules = [AppModule::class, NetworkModule::class, DatabaseModule::class]) interface AppComponent { fun inject(app: App) fun activityComponentBuilder(): ActivityComponent.Builder } @Module class NetworkModule { @Provides @Singleton fun provideOkHttpClient(): OkHttpClient { return OkHttpClient.Builder() .addInterceptor(AuthInterceptor()) .connectTimeout(30, TimeUnit.SECONDS) .build() } @Provides @Singleton fun provideRetrofit(client: OkHttpClient): Retrofit { return Retrofit.Builder() .baseUrl(BuildConfig.API_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build() } } Why are scopes a common cause of elusive bugs?
The most frequent mistake is incorrect scopes. If UserRepository is declared @Singleton and AuthToken inside it is stored in memory, then after logout, without recreating the component, the old token remains alive. This leads to requests with a stale token — a production bug that only reproduces under a specific scenario. Solution: @Singleton components should not contain mutable state that depends on the user session. Session-scoped dependencies should be moved to @UserScope:
@Scope @Retention(AnnotationRetention.RUNTIME) annotation class UserScope @UserScope @Subcomponent(modules = [UserModule::class]) interface UserComponent { @Subcomponent.Factory interface Factory { fun create(@BindsInstance userId: String): UserComponent } fun inject(profileFragment: ProfileFragment) } UserComponent is created after login and destroyed after logout. All dependencies bound to the user live exactly as long as needed. Below is a table of typical mistakes:
| Typical Mistake | Consequence | Solution |
|---|---|---|
| Mutable state in @Singleton | Data leak on logout | Move to @UserScope |
| @Singleton dependency with slow initialization | App startup delay | Use @Lazy or Provider |
| Missing @BindsInstance | Need to manually create component | Add @BindsInstance for dynamic parameters |
Additionally, improper use of scopes can cause memory leaks due to accidentally holding an Activity context in @Singleton. We always check the graph for such scenarios.
Multibindings and plugin architecture
@IntoMap with @ViewModelKey is a pattern for injecting ViewModels via ViewModelProvider.Factory. Dagger creates a Map<Class<out ViewModel>, Provider<ViewModel>>, and the factory selects the correct class. Without this pattern, each ViewModel must be declared separately in the component.
@Module abstract class ViewModelModule { @Binds @IntoMap @ViewModelKey(LoginViewModel::class) abstract fun bindLoginViewModel(vm: LoginViewModel): ViewModel @Binds @IntoMap @ViewModelKey(ProfileViewModel::class) abstract fun bindProfileViewModel(vm: ProfileViewModel): ViewModel } This same approach applies to plugin architecture, where each module registers its dependencies via @IntoSet or @IntoMap. We used this in a project with 10+ feature modules — Dagger automatically collects all provided implementations.
Kapt and KSP: which to choose for builds?
Dagger 2 traditionally works with kapt. Starting from version 2.50, experimental support for KSP is available, which speeds up incremental builds. On a project with ~200 Dagger annotations, switching from kapt to KSP reduced clean build time from 4.5 to 2.8 minutes — a significant development time saving.
// build.gradle.kts plugins { id("com.google.devtools.ksp") } dependencies { implementation("com.google.dagger:dagger:2.51") ksp("com.google.dagger:dagger-compiler:2.51") } | Parameter | Kapt | KSP (experimental) |
|---|---|---|
| Clean build time (200 annotations) | 4.5 min | 2.8 min |
| Incremental build | Normal | Accelerated |
| Dagger support | Full | Since 2.50, not all features |
Also, don't forget about obfuscation: ProGuard/R8 can remove classes generated by Dagger if keep rules are not added. We include this in the configuration.
How to test an app with Dagger?
Dagger and tests are a separate story. The standard approach: test modules replace production dependencies with fakes:
@Component(modules = [TestNetworkModule::class, DatabaseModule::class]) interface TestAppComponent : AppComponent @Module class TestNetworkModule { @Provides @Singleton fun provideApiService(): ApiService = FakeApiService() } In Espresso tests, DaggerTestAppComponent is substituted for the main one in App.appComponent before the test runs. Without this replacement, integration tests hit the real server. We also use TestCoroutineDispatcher to simulate delays.
When to choose Dagger 2 over Hilt
Hilt is a wrapper over Dagger with a predefined component structure. If you need custom scopes, multi-module graphs with independent components, or Dagger is already in the project — Dagger 2 gives full control. Hilt gets you started faster, but limits complex architectures. In our projects, we often combine Dagger with Hilt in different modules.
What is included in a turnkey Dagger 2 setup
- Analysis of architecture and component graph design
- Creation of modules for network, database, shared preferences
- Scope configuration (Singleton, PerActivity, UserScope)
- ViewModel integration via multibinding
- Test component setup with fakes
- Migration from kapt to KSP if needed
- Documentation for graph maintenance
The cost is calculated individually. Setup from scratch — 3–5 days, refactoring — from a week. Get a consultation: write to us, we'll assess your project. Want to implement Dagger 2 without headaches? Contact us, we'll audit your DI code.







