What Dagger Actually Is
Dagger is a dependency injection framework originally built by Square and now maintained by Google. It runs on the JVM and is most commonly used in Android projects, though it also works for Java and Kotlin backend applications. The core idea is that you declare what your classes need, and Dagger generates the code that wires everything together at compile time instead of at runtime.
The reason people choose it over alternatives like Hilt or Koin comes down to performance and predictability. Dagger catches dependency graph errors before your app even launches. I've seen projects waste weeks chasing null pointer exceptions that Dagger would have rejected in the compilation step if they had switched earlier.
History Of The Dagger
Dagger started as an open source project by Square in 2012. The first public release was Dagger 1, which was written entirely in Java and required manual annotation processing setup. It worked but had significant limitations around scope management and module composition. The community found it powerful but verbose, and adoption grew slowly in the Android ecosystem.
Dagger 2 arrived in 2015 as a complete rewrite. The biggest shift was that the annotation processor generated all the boilerplate code rather than relying on reflection. This meant faster startup times and smaller APK sizes since no reflection libraries were bundled. It also introduced scopes like @Singleton and @ActivityScoped which became standard patterns in Android architecture.
Google adopted Dagger in 2017 and integrated it more tightly with the Android toolchain. The framework has since reached version 2.51 as of mid-2026, with ongoing improvements to Kotlin compatibility and incremental compilation performance.
How to Set Up Dagger in a New Project
Start by adding the dependencies to your build configuration. For Android projects using Gradle, you need the compiler artifact and the runtime API. In your module-level build.gradle file, add the compiler as an implementation dependency and the runtime as api. The compiler artifact is what generates the Dagger code during build. The runtime is what your application code actually uses.
Next, create your first module class. A module is simply a class annotated with @Module that contains @Provides methods. Each method returns an object and declares what type it provides. Dagger reads these annotations and builds a dependency graph from them. Here is what a basic module looks like:
@Module
class NetworkModule {
@Provides
fun provideBaseUrl(): String {
return "https://api.example.com"
}
}
Your component interface tells Dagger what modules to include and what types it should be able to inject. The component is the bridge between your declared dependencies and the generated code. Without a component, Dagger has nowhere to attach the graph.
@Component(modules = [NetworkModule::class])
interface AppComponent {
fun inject(activity: MainActivity)
}
The generated code will implement this interface. You call methods on it to access dependencies. In your activity or fragment, you add @Inject annotations to fields or constructors, and Dagger fills them in when you call the inject method on the component instance.
The common mistake beginners make is putting all dependencies into one giant module and component. This works until the project grows past a few screens, then the component becomes impossible to maintain. Break it into smaller components with narrower scopes. Use subcomponents when you need dependency isolation between features.
The Annotation Processor Setup Problem
Annotation processing in Android has changed several times across Gradle plugin versions, and this is where most people run into trouble. If you are using AGP 7.0 or later, the default annotation processor is kapt for Kotlin projects. Kapt works but has a notable downside: it processes the entire project on every build, even when you only changed one file. This makes incremental builds significantly slower than they should be.
The workaround I use on every project is switching to KSP, the Kotlin Symbol Processing API. KSP is noticeably faster because it does not require a separate compilation round. It also handles Kotlin syntax more accurately than kapt does. To switch, you replace the kapt dependency declaration with an ksp one and point it at the Dagger compiler artifact. The functionality stays identical, but build times drop considerably on medium to large projects.
For projects still on older Gradle plugin versions, you can also configure the annotation processor path directly. Set the javaCompiler task to point at the Dagger compiler JAR. This bypasses kapt entirely and runs the processor in the same JVM thread as compilation. It is not the cleanest setup but it works reliably across different IDE versions.
Scope Management — What Actually Happens
Scopes control the lifetime of injected objects. When you annotate a binding with @Singleton, Dagger creates exactly one instance and reuses it across the entire component lifecycle. When you use a custom scope like @FragmentScoped, a new instance is created each time a new component with that scope is built.
The subtle issue is that scope mismatches are caught at compile time but the resulting bugs are insidious. If you accidentally bind a singleton-scoped dependency into a request-scoped component, you will get a compile error. If you bind a request-scoped dependency into a singleton component, Dagger silently allows it and you end up with stale state. I encountered this exact problem on a project where a repository holding user session data was incorrectly scoped, causing cached authentication tokens to persist across user logout events for the entire app lifetime. The fix was tracing the dependency chain back to the @Module and changing the scope annotation on the provider method, not on the component.
Common Pitfalls and How to Avoid Them
Circular dependencies will crash your build. Dagger detects them at compile time and gives you an error message, but the message is not always clear about which exact bindings are involved. When this happens, trace the dependency chain manually. The typical solution is to break the cycle by introducing an intermediate provider or by redesigning the dependency structure so one side does not depend on the other.
Provider methods that accept parameters must have those parameters available in the graph. If you write a @Provides method that takes a Context, Dagger will look for a binding that provides Context. If none exists, the build fails. The standard Android workaround is to add a @Provides method in an Application-level module that returns the application context, then reference that method in your component.
Multibindings are powerful but underused. They let you collect multiple implementations of the same interface into a set or map at runtime. This is useful for plugin systems, feature flags, and routing tables. The syntax is straightforward but easy to get wrong. You need @IntoSet or @IntoMap annotations on the provider methods, and the component must declare a multibinding method that returns a Collection or Map of the target type.
When Dagger Is the Wrong Tool
Dagger adds build complexity. For small projects or prototypes, the overhead of setting up modules, components, and scopes often outweighs the benefits. Simpler frameworks like Koin for Kotlin projects or even manual dependency passing can be more practical. Hilt, which is built on top of Dagger, reduces some of the boilerplate but adds its own layer of complexity that may not be justified for simple use cases.
The tradeoff is real. You gain compile-time safety and performance, but you lose flexibility. Refactoring a Dagger-based architecture requires understanding the generated code and the annotation processor output. If your team does not have someone who understands the framework deeply, maintenance costs accumulate quickly. I have seen teams spend more time debugging Dagger configuration issues than they would have spent debugging runtime dependency problems with a simpler approach.
If you are evaluating whether to use Dagger, the decision should come down to project scale and team expertise. Large codebases with complex dependency structures benefit most. Small to medium projects often do better with something lighter.