Skip to content

Dagger Mental Model

Andy Boothe edited this page Jan 17, 2025 · 2 revisions

Dagger is an enormously useful library, but its documentation is not great, in my experience. Here is how I think about Dagger, for whatever that's worth, shared here in the hopes that it will be useful to others.


Components create "holes"

Dagger @Component interfaces represent "holes" in your application's dependency graph. These holes declare the objects you need to obtain or inject.

Example:

@Component
public interface MyComponent {
    MyService getMyService();
}
  • What it does: The MyComponent interface creates a "hole" for MyService. Dagger needs to know how to provide an instance of MyService.

Modules create "pegs"

Modules define how to "fill" the holes in the dependency graph by providing the necessary objects.

Example:

@Module
public class MyModule {
    @Provides
    MyService provideMyService() {
        return new MyService();
    }
}

Putting It Together:

@Component(modules = MyModule.class)
public interface MyComponent {
    MyService getMyService();
}

// Application Code:
public class Main {
    public static void main(String[] args) {
        MyComponent component = DaggerMyComponent.create();
        MyService myService = component.getMyService();
        System.out.println("Service: " + myService);
    }
}
  • What it does: The @Module provides a "peg" (MyService) that Dagger uses to fulfill the "hole" in the @Component.

Modules can also have holes

Modules can depend on values or objects that must be provided by other parts of the graph, creating their own "holes."

Example:

@Module
public class ConfigModule {
    private final String configValue;

    public ConfigModule(String configValue) {
        this.configValue = configValue;
    }

    @Provides
    String provideConfigValue() {
        return configValue;
    }
}

@Module
public class MyModule {
    @Provides
    MyService provideMyService(String configValue) {
        return new MyService(configValue);
    }
}

@Component(modules = {ConfigModule.class, MyModule.class})
public interface MyComponent {
    MyService getMyService();
}

Application Code:

public class Main {
    public static void main(String[] args) {
        MyComponent component = DaggerMyComponent.builder()
                .configModule(new ConfigModule("MyConfigValue"))
                .build();
        MyService myService = component.getMyService();
        System.out.println("Service created with config: " + myService.getConfigValue());
    }
}

Factory Methods

Modules can depend on values provided by other modules, using factory methods for their creation.

Example with Dependencies Between Modules:

@Module
public class DatabaseModule {
    @Provides
    Database provideDatabase() {
        return new Database("localhost", 5432);
    }
}

@Module
public class RepositoryModule {
    @Provides
    Repository provideRepository(Database database) {
        return new Repository(database);
    }
}

@Component(modules = {DatabaseModule.class, RepositoryModule.class})
public interface MyComponent {
    Repository getRepository();
}
  • What it does: DatabaseModule provides a Database, and RepositoryModule depends on it to create a Repository.

Constructor Parameters

Modules can depend on values passed to their constructor, implementing Inversion of Control (IoC).

Example:

@Module
public class ConfigModule {
    private final String apiUrl;

    public ConfigModule(String apiUrl) {
        this.apiUrl = apiUrl;
    }

    @Provides
    String provideApiUrl() {
        return apiUrl;
    }
}

@Component(modules = ConfigModule.class)
public interface MyComponent {
    ApiClient getApiClient();

    @Component.Factory
    interface Factory {
        MyComponent create(ConfigModule configModule);
    }
}

Application Code:

public class Main {
    public static void main(String[] args) {
        MyComponent component = DaggerMyComponent.factory()
                .create(new ConfigModule("https://api.example.com"));
        ApiClient apiClient = component.getApiClient();
        System.out.println("API URL: " + apiClient.getApiUrl());
    }
}

Objects being provided can also have holes

Objects provided by Dagger can also have their own dependencies (holes), which are filled by the graph.

Example:

public class MyService {
    private final Dependency dependency;

    @Inject
    public MyService(Dependency dependency) {
        this.dependency = dependency;
    }
}

@Module
public class MyModule {
    @Provides
    Dependency provideDependency() {
        return new Dependency();
    }
}

@Component(modules = MyModule.class)
public interface MyComponent {
    MyService getMyService();
}
  • What it does: Dagger resolves Dependency and injects it into MyService.

Advanced Usage

Subcomponents

Subcomponents are used for parent-child lifecycle relationships. The relationship is explicitly declared at compile time. Controlling object lifecycles involves using scopes.


Component Dependencies

Loose Coupling

Component dependencies promote modularity by allowing components to depend on others without tightly coupling their implementation.

Example:

@Component
public interface LibraryComponent {
    LibraryService getLibraryService();
}

@Component(dependencies = LibraryComponent.class)
public interface AppComponent {
    AppService getAppService();
}
  • What it does: The AppComponent depends on LibraryComponent to provide some of its dependencies.

DRY (Don't Repeat Yourself)

Component dependencies can reuse object graphs for shared logic across components.

Example:

@Component
public interface SharedComponent {
    Database getDatabase();
}

@Component(dependencies = SharedComponent.class)
public interface AppComponent {
    WebResource getWebResource();
}

public class WebResource {
    @Inject
    public WebResource(Database database) {
    }
}

Library

Component dependencies allow "extending" functionality provided by another component.

Example:

A library might provide its own component (e.g., LibraryComponent), and the application can make use of the object graph it provides.

@Component
public interface LibraryComponent {
    LibraryService getLibraryService();
}

@Component(dependencies = LibraryComponent.class)
public interface ApplicationComponent {
    AppService getAppService();
}
  • Use Case: When building modular or library-based systems.