Dependency Injection with Dagger in Java

Dagger is a compile-time dependency injection framework for Java and Android that generates code to perform dependency injection. This comprehensive guide covers Dagger 2 usage, components, modules, scopes, and best practices.

Basic Dagger Setup

1. Dependencies Configuration

Maven:

<properties>
<dagger.version>2.48.1</dagger.version>
</properties>
<dependencies>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
<version>${dagger.version}</version>
</dependency>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<version>${dagger.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>

Gradle:

dependencies {
implementation 'com.google.dagger:dagger:2.48.1'
annotationProcessor 'com.google.dagger:dagger-compiler:2.48.1'
}

2. Basic Dependency Injection

Simple Classes:

// Domain model
public class User {
private final String name;
private final String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
// Getters
public String getName() { return name; }
public String getEmail() { return email; }
}
// Repository interface
public interface UserRepository {
User findById(Long id);
void save(User user);
}
// Repository implementation
public class UserRepositoryImpl implements UserRepository {
@Override
public User findById(Long id) {
// Implementation to fetch user from database
return new User("John Doe", "[email protected]");
}
@Override
public void save(User user) {
// Implementation to save user to database
System.out.println("Saving user: " + user.getName());
}
}
// Service class that depends on repository
public class UserService {
private final UserRepository userRepository;
@Inject
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUser(Long id) {
return userRepository.findById(id);
}
public void createUser(String name, String email) {
User user = new User(name, email);
userRepository.save(user);
}
}

Dagger Modules and Components

3. Basic Module and Component

AppModule.java:

import dagger.Module;
import dagger.Provides;
import javax.inject.Singleton;
@Module
public class AppModule {
@Provides
@Singleton
public UserRepository provideUserRepository() {
return new UserRepositoryImpl();
}
@Provides
@Singleton
public UserService provideUserService(UserRepository userRepository) {
return new UserService(userRepository);
}
}

AppComponent.java:

import dagger.Component;
import javax.inject.Singleton;
@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {
UserService getUserService();
UserRepository getUserRepository();
// Factory method to create instances
static AppComponent create() {
return DaggerAppComponent.create();
}
}

4. Using Dagger in Application

MainApplication.java:

public class MainApplication {
private static AppComponent appComponent;
public static void main(String[] args) {
// Initialize Dagger component
appComponent = AppComponent.create();
// Use injected dependencies
UserService userService = appComponent.getUserService();
User user = userService.getUser(1L);
System.out.println("User: " + user.getName() + " - " + user.getEmail());
userService.createUser("Jane Smith", "[email protected]");
}
public static AppComponent getAppComponent() {
return appComponent;
}
}

Advanced Dagger Features

5. Multiple Modules and Component Dependencies

DatabaseModule.java:

@Module
public class DatabaseModule {
@Provides
@Singleton
public DataSource provideDataSource() {
// Configure and return DataSource
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
dataSource.setUsername("user");
dataSource.setPassword("password");
return dataSource;
}
@Provides
@Singleton
public JdbcTemplate provideJdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}

NetworkModule.java:

@Module
public class NetworkModule {
private final String baseUrl;
public NetworkModule(String baseUrl) {
this.baseUrl = baseUrl;
}
@Provides
@Singleton
public OkHttpClient provideOkHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
}
@Provides
@Singleton
public Retrofit provideRetrofit(OkHttpClient okHttpClient) {
return new Retrofit.Builder()
.baseUrl(baseUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
@Provides
@Singleton
public UserApi provideUserApi(Retrofit retrofit) {
return retrofit.create(UserApi.class);
}
}

Advanced AppComponent.java:

@Singleton
@Component(modules = {
AppModule.class,
DatabaseModule.class,
NetworkModule.class
})
public interface AppComponent {
UserService getUserService();
DataSource getDataSource();
UserApi getUserApi();
// Component builder for modules with constructor parameters
@Component.Builder
interface Builder {
Builder networkModule(NetworkModule networkModule);
Builder appModule(AppModule appModule);
Builder databaseModule(DatabaseModule databaseModule);
AppComponent build();
}
static AppComponent create() {
return DaggerAppComponent.builder()
.appModule(new AppModule())
.databaseModule(new DatabaseModule())
.networkModule(new NetworkModule("https://api.example.com/"))
.build();
}
}

6. @Binds and Abstract Modules

ServiceModule.java:

@Module
public abstract class ServiceModule {
@Binds
public abstract UserRepository bindUserRepository(UserRepositoryImpl impl);
@Binds
@Singleton
public abstract NotificationService bindNotificationService(
EmailNotificationService impl);
}
// Interface and implementation
public interface NotificationService {
void sendNotification(String message, String recipient);
}
public class EmailNotificationService implements NotificationService {
@Inject
public EmailNotificationService() {}
@Override
public void sendNotification(String message, String recipient) {
System.out.println("Sending email to " + recipient + ": " + message);
}
}

Scopes and Custom Scopes

7. Custom Scopes

Custom Scopes:

import javax.inject.Scope;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface ApplicationScope {}
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestScope {}
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface SessionScope {}

Scoped Modules:

@Module
public class ScopedModule {
@Provides
@ApplicationScope
public ApplicationConfig provideApplicationConfig() {
return new ApplicationConfig();
}
@Provides
@RequestScope
public RequestContext provideRequestContext() {
return new RequestContext();
}
@Provides
@SessionScope
public UserSession provideUserSession() {
return new UserSession();
}
}

Constructor Injection vs Field Injection

8. Different Injection Methods

Constructor Injection (Recommended):

public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final NotificationService notificationService;
@Inject
public OrderService(OrderRepository orderRepository,
PaymentService paymentService,
NotificationService notificationService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
this.notificationService = notificationService;
}
}

Field Injection:

public class ShoppingCartController {
@Inject
private ShoppingCartService shoppingCartService;
@Inject
private ProductService productService;
@Inject
private UserService userService;
// Must be called after instance creation to inject fields
@Inject
public void setupDependencies() {
// Optional setup method called after field injection
}
}

Method Injection:

public class ReportGenerator {
private DataFormatter dataFormatter;
private TemplateEngine templateEngine;
@Inject
public void setDependencies(DataFormatter dataFormatter,
TemplateEngine templateEngine) {
this.dataFormatter = dataFormatter;
this.templateEngine = templateEngine;
}
}

Qualified Dependencies

9. Using @Named and @Qualifier

Custom Qualifiers:

import javax.inject.Qualifier;
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface DatabaseUrl {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiKey {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface Production {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface Development {}

Qualified Providers:

@Module
public class ConfigurationModule {
@Provides
@DatabaseUrl
public String provideDatabaseUrl() {
return "jdbc:mysql://localhost:3306/production";
}
@Provides
@ApiKey
public String provideApiKey() {
return System.getenv("API_KEY");
}
@Provides
@Production
public DataSource provideProductionDataSource(@DatabaseUrl String url) {
return createDataSource(url, "prod_user", "prod_pass");
}
@Provides
@Development
public DataSource provideDevelopmentDataSource() {
return createDataSource("jdbc:h2:mem:testdb", "sa", "");
}
private DataSource createDataSource(String url, String user, String password) {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl(url);
ds.setUsername(user);
ds.setPassword(password);
return ds;
}
}

Using Qualified Dependencies:

public class ApplicationService {
private final DataSource productionDataSource;
private final DataSource developmentDataSource;
private final String apiKey;
@Inject
public ApplicationService(@Production DataSource productionDataSource,
@Development DataSource developmentDataSource,
@ApiKey String apiKey) {
this.productionDataSource = productionDataSource;
this.developmentDataSource = developmentDataSource;
this.apiKey = apiKey;
}
}

Component Dependencies and Subcomponents

10. Subcomponents for Scoped Dependencies

RequestComponent.java:

@RequestScope
@Subcomponent(modules = RequestModule.class)
public interface RequestComponent {
// Factory method to create instances
@Subcomponent.Factory
interface Factory {
RequestComponent create();
}
// Methods to expose dependencies
RequestContext getRequestContext();
UserService getUserService();
OrderService getOrderService();
// Inject into classes
void inject(OrderController orderController);
}

RequestModule.java:

@Module
public class RequestModule {
@Provides
@RequestScope
public RequestContext provideRequestContext() {
return new RequestContext();
}
@Provides
@RequestScope
public OrderService provideOrderService(UserService userService,
PaymentService paymentService) {
return new OrderService(userService, paymentService);
}
}

Main Application Component:

@Singleton
@Component(modules = {
AppModule.class,
DatabaseModule.class,
NetworkModule.class
})
public interface ApplicationComponent {
// Expose factory for subcomponents
RequestComponent.Factory requestComponent();
// Other dependencies...
UserService getUserService();
PaymentService getPaymentService();
}

Usage in Web Framework:

public class OrderController {
private final RequestComponent requestComponent;
@Inject
public OrderController(ApplicationComponent appComponent) {
this.requestComponent = appComponent.requestComponent().create();
this.requestComponent.inject(this);
}
public void processOrder(Order order) {
OrderService orderService = requestComponent.getOrderService();
orderService.process(order);
}
}

Dagger in Web Applications

11. Spring Boot Integration

SpringBootDaggerIntegration.java:

@Configuration
public class DaggerConfig {
@Bean
@Singleton
public ApplicationComponent applicationComponent() {
return DaggerApplicationComponent.builder()
.appModule(new AppModule())
.databaseModule(new DatabaseModule())
.networkModule(new NetworkModule("https://api.example.com"))
.build();
}
}
@Service
public class UserService {
private final com.example.dagger.UserService daggerUserService;
@Inject
public UserService(ApplicationComponent applicationComponent) {
this.daggerUserService = applicationComponent.getUserService();
}
public User findUser(Long id) {
return daggerUserService.getUser(id);
}
}
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@Inject
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.findUser(id);
return ResponseEntity.ok(user);
}
}

Testing with Dagger

12. Test Modules and Components

TestDatabaseModule.java:

@Module
public class TestDatabaseModule {
@Provides
@Singleton
public DataSource provideTestDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("test-schema.sql")
.build();
}
@Provides
@Singleton
public UserRepository provideTestUserRepository() {
return new MockUserRepository();
}
}

TestAppComponent.java:

@Singleton
@Component(modules = {
TestDatabaseModule.class,
NetworkModule.class
})
public interface TestAppComponent extends AppComponent {
@Component.Builder
interface Builder {
Builder testDatabaseModule(TestDatabaseModule module);
Builder networkModule(NetworkModule module);
TestAppComponent build();
}
}

Unit Tests:

public class UserServiceTest {
private UserService userService;
private TestAppComponent testComponent;
@BeforeEach
void setUp() {
testComponent = DaggerTestAppComponent.builder()
.testDatabaseModule(new TestDatabaseModule())
.networkModule(new NetworkModule("http://test.api.com"))
.build();
userService = testComponent.getUserService();
}
@Test
void testGetUser() {
User user = userService.getUser(1L);
assertNotNull(user);
assertEquals("Test User", user.getName());
}
}
// Mock implementation for testing
class MockUserRepository implements UserRepository {
@Override
public User findById(Long id) {
return new User("Test User", "[email protected]");
}
@Override
public void save(User user) {
// Mock implementation
}
}

Advanced Patterns

13. Provider Injection and Lazy Injection

Provider and Lazy Usage:

public class OrderProcessor {
private final Provider<OrderService> orderServiceProvider;
private final Lazy<ReportService> reportServiceLazy;
private final Set<Provider<EventListener>> eventListeners;
@Inject
public OrderProcessor(Provider<OrderService> orderServiceProvider,
Lazy<ReportService> reportServiceLazy,
Set<Provider<EventListener>> eventListeners) {
this.orderServiceProvider = orderServiceProvider;
this.reportServiceLazy = reportServiceLazy;
this.eventListeners = eventListeners;
}
public void processOrder(Order order) {
// Get new instance each time
OrderService orderService = orderServiceProvider.get();
orderService.process(order);
// Get instance only when needed (lazy initialization)
ReportService reportService = reportServiceLazy.get();
reportService.generateReport(order);
// Multiple implementations
eventListeners.forEach(provider -> {
EventListener listener = provider.get();
listener.onOrderProcessed(order);
});
}
}

14. Multibinding for Multiple Implementations

Multibinding Module:

@Module
public abstract class EventListenerModule {
@Binds
@IntoSet
public abstract EventListener bindEmailEventListener(EmailEventListener impl);
@Binds
@IntoSet
public abstract EventListener bindSmsEventListener(SmsEventListener impl);
@Binds
@IntoSet
public abstract EventListener bindPushNotificationEventListener(
PushNotificationEventListener impl);
}
// Usage
public class EventDispatcher {
private final Set<EventListener> eventListeners;
@Inject
public EventDispatcher(Set<EventListener> eventListeners) {
this.eventListeners = eventListeners;
}
public void dispatchEvent(Event event) {
eventListeners.forEach(listener -> listener.handle(event));
}
}

Best Practices and Common Patterns

15. Dagger Best Practices

Component Organization:

// Good: Organized by feature or layer
@Singleton
@Component(modules = {
DataModule.class,      // Database and repository layer
ServiceModule.class,   // Business logic layer  
WebModule.class,       // Web and API layer
ConfigModule.class     // Configuration
})
public interface ApplicationComponent {
// Expose only what's needed
UserService getUserService();
OrderService getOrderService();
}
// Good: Use @Binds for interface implementations
@Module
public abstract class ServiceModule {
@Binds
public abstract UserRepository bindUserRepository(UserRepositoryImpl impl);
@Binds
public abstract PaymentService bindPaymentService(StripePaymentService impl);
}
// Good: Scoped dependencies
@Module
public class SessionModule {
@Provides
@SessionScope
public UserSession provideUserSession() {
return new UserSession();
}
}

Error-Prone Patterns to Avoid:

// ❌ Avoid: Circular dependencies
public class ServiceA {
@Inject
public ServiceA(ServiceB serviceB) { }
}
public class ServiceB {
@Inject 
public ServiceB(ServiceA serviceA) { } // Circular dependency!
}
// ✅ Solution: Use Provider or refactor
public class ServiceA {
@Inject
public ServiceA(Provider<ServiceB> serviceBProvider) { }
}
// ❌ Avoid: Too many constructor parameters
public class Service {
@Inject
public Service(A a, B b, C c, D d, E e, F f, G g) { } // Too many!
}
// ✅ Solution: Split into smaller services or use factories
public class Service {
@Inject
public Service(ServiceDependencies deps) { }
}
@Module
public class ServiceDependenciesModule {
@Provides
public ServiceDependencies provideDependencies(A a, B b, C c) {
return new ServiceDependencies(a, b, c);
}
}

Key Benefits of Dagger

  1. Compile-time Safety: Errors are caught at compile time rather than runtime
  2. Performance: No reflection, generated code is efficient
  3. Testability: Easy to replace dependencies with test doubles
  4. Scalability: Handles complex dependency graphs
  5. Maintainability: Clear dependency structure and relationships
  6. No Boilerplate: Reduces manual dependency management code

Dagger provides a robust, type-safe dependency injection solution that's particularly valuable in large-scale applications where maintainability and testability are crucial.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper