Module 2: Dependency Injection 1

Module Overview

Introduction to dependency injection and its benefits in Java applications. Learn the foundational concepts and patterns for implementing DI.

Learning Objectives

  • Use a dependency injection framework to provide an object with its dependencies
  • Analyze if an injected object should be scoped as a singleton or not for a provided scenario
  • Identify what types a given Java class depends on
  • Illustrate object dependency relationships as a dependency graph
  • Explain what dependency injection is
  • Explain why dependency injection is preferable to having objects instantiate their own dependencies

Video Content: Dependency Injection Motivation

This video explains why dependency injection is important and how it solves problems with tightly coupled code. Understanding the motivation behind DI helps appreciate its value in software design.

// Without dependency injection - tight coupling
public class UserService {
    // Direct instantiation creates tight coupling
    private UserRepository userRepository = new MySQLUserRepository();
    
    public User getUserById(String id) {
        return userRepository.findById(id);
    }
}

// With dependency injection - loose coupling
public class UserService {
    // Dependency is injected, not created internally
    private final UserRepository userRepository;
    
    // Constructor injection
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public User getUserById(String id) {
        return userRepository.findById(id);
    }
}

This example contrasts two approaches: one with hardcoded dependencies (tight coupling) and another with injected dependencies (loose coupling). The second approach is more flexible, testable, and maintainable because the UserService doesn't need to know the concrete implementation of UserRepository.

We are going to learn a new concept, dependency injection, which allows us to better isolate our classes and test them more easily. The core idea is to provide the member variables for a class through its constructor, rather than letting the class instantiate its own objects.

We will also learn about a framework called Dagger that makes dependency injection easier to manage on larger projects.

What is a dependency?

In prior readings, we have talked a little about dependencies. For example, a DAO (Data Access Object) loading data from DynamoDB depends on a DynamoDBMapper object to communicate with DynamoDB. This is an example of designing with composition: we can say our DAO has a DynamoDBMapper. We can further say that the DAO has a dependency. This relationship is called a dependency because one object depends on another contained object to fulfill its functionality. Modifying the class that provides this functionality would require changes to the class that depends on it.

Let's take a sample application as an example:

// ZooApplication.java
public class ZooApplication {
    public static void main(String[] args) {
        ZooService zooService = new ZooService();

        zooService.addNewAnimal("Ring-Tailed Lemur");
        //...
    }       
}

// ZooZervice.java
public class ZooService {
    private DynamoDbAnimalDao animalDao;

    public ZooService() {
        this.animalDao = new DynamoDbAnimalDao();    
    }   

    public void addNewAnimal(String animalType) {
        //  Create and save new animal with the type passed into the method
        Animal animal = new Animal(animalType);
        animalDao.save(animal);
    }
}

// DynamoDbAnimalDao.java
public class DynamoDbAnimalDao {
    private DynamoDBMapper dynamoDBMapper;

    public DynamoDbAnimalDao() {
       this.dynamoDBMapper =
           new DynamoDBMapper(DynamoDbClientProvider.provideDynamoDBClient(Regions.US_WEST_2));
    }

    public void save(Animal animal) {
        this.dynamoDBMapper.save(animal);
    }
}

Dependency graphs

Notice the first line inside the main method in the ZooApplication class. ZooService zooService = new ZooService();. The ZooService object depends on a DynamoDbAnimalDao. If we continue the dependency chain, we see that DynamoDbAnimalDao has yet another dependency, DynamoDBMapper. It is good that we can separate responsibility across the different classes, but by creating each dependency in the class's constructor, it's difficult to test any one class individually. This is because we are unable to pass the mock to the object that depends on it if the object is instantiating its own dependencies.

We can visualize this chain of dependencies like so:

Diagram that shows the dependency graph as a straight chain. ZooService ➜ DynamoDbAnimalDao ➜ DynamoDBMapper

Figure 1: Diagram that shows the dependency graph as a straight chain.

We can imagine that an object might depend on more than one dependency, and we generalize this notion of a "dependency chain" to a dependency graph, which just means one object can point to more than one other object.

We'll learn more about graphs in a later unit, but for now, just remember that a "graph" is a drawing of things that point to each other. In the dependency graph above, ZooService points to DynamoDbAnimalDao, which points to DynamoDBMapper, so we would say:

  • ZooService depends directly on DynamoDbAnimalDao
  • ZooService's only direct dependency is DynamoDbAnimalDao
  • DynamoDBMapper is in ZooService's dependency graph

Root object of a dependency graph

You may have noticed that we excluded the ZooApplication class from this dependency graph. We never actually instantiate a ZooApplication instance, so we don't need to solve the problem of providing an instance of the class. It's the class that requires the first object in our dependency graph, ZooService. Once our application has an instance of ZooService, it can perform its duties. ZooApplication doesn't actually care about all of ZooService's dependencies.

This is because ZooService is what's called the root of the dependency graph. It is an object that has dependencies, but isn't depended upon by any other objects. Just as the root of a biological tree is where the tree "starts" and grows from, the root of a dependency chain or graph is an object that starts the dependency relationships. It's the object our application code can ask for, and get all of its dependencies along with it.

Practically speaking, an application only needs to be provided its dependency graph's root objects. (We say "root objects" because the application class might require more than one object, each being a root of its own dependency graph.) The application class should not need to worry about instantiating all of the dependency objects as long as it can be given the roots. This will be an important concept to return to later when we introduce our dependency injection framework.

How you provide a class's dependency does matter

One necessary step in composing objects in this way is for a new instance of an object to obtain instances of its dependency objects. A natural approach to this problem is for a class to instantiate all of its dependency objects by itself in its own constructor. After all, what code would know better how to provide a ZooApplication class with its needs than the ZooApplication class itself?

This is the way we implemented the example above. ZooService has a direct dependency on DynamoDbAnimalDao.

In the future, we could expand our code to handle new situations, such as a butterfly house or an aquarium. We could build an AnimalDao interface that DynamoDbAnimalDao implemented. We could then create concrete classes for different purposes and different environments. Then ZooService could use AnimalDao, and polymorphism would keep the ZooService code unaware of which type of AnimalDao it is using.

To take advantage of polymorphism, we would need to find a way to change which concrete implementation of AnimalDao the ZooService used. ZooService itself could certainly do this, perhaps in its constructor. If the environment is an aquarium, then use the AquaticAnimalDao class, but otherwise use the DynamoDbAnimalDao class etc.

But this eliminates the advantage of a generic interface! The ZooService now needs to know about every possible kind of AnimalDao. What we'd really like is for some other code to decide if we're in a particular environment and provide the appropriate objects to the ZooService class, removing that instantiation logic from ZooService altogether.

When classes instantiate their own dependency objects, the dependency instantiation logic is mixed up with the actual class logic.

We always use classes in at least two environments: production and test. Unit tests should test only the class under test. This means we should exclude the dependency classes where possible, and not redundantly or confusingly test their behavior at the same time.

In production environments, we want the ZooService to call the actual DynamoDbAnimalDao. In test environments, we want to control the DynamoDbAnimalDao's behavior so we can simulate exceptions and edge cases.

If the class instantiates its own dependency objects, we need to modify its logic to support testing behaviors. Then we're weirdly mixing test code and production code in the same classes. This can lead to developer mistakes: for example, a developer can mistakenly call a test-code method from production code. This can lead to very unexpected behavior!

Dependency Injection to the rescue!

A great solution to these problems is to pass an object its dependency objects in some way. This is the whole idea of Dependency Injection: we pass an object its dependency objects so that the class doesn't have to build them itself. Recall the concept of "Inversion of Control" we touched on in Unit 1. We are responsible for the behavior of our classes, but we are handing off the responsibility of managing and wiring together our classes to our dependency injection strategy. This allows us to apply the unit testing and mocking best practices that we have learned. As hinted at in the polymorphism discussion above, it can also allow different behaviors in the production environment. We can remove the logic for determining which subclass or configuration to use for the dependency objects from the class itself.

Here is one version of what that might look like, very similar to the App-class pattern we have used in the ATA projects so far. We pass each object its dependency objects in its constructor. Our App class does a bunch of instantiation, ultimately providing the root object that the ZooApplication needs, a ZooService.

Notice that ZooApplication just calls one method, and it gets the object it needs. With this implementation, ZooApplication does not need to:

  • instantiate a DynamoDBMapper object
  • instantiate a concrete DynamoDbAnimalDao object, using the DynamoDBMapper object,
  • instantiate a ZooService object, using the DynamoDbAnimalDao object
public class ZooAppHelper {
    public ZooService provideZooService() {
        return new ZooService(provideDynamoDbAnimalDao());        
    }

    private DynamoDbAnimalDao provideDynamoDbAnimalDao() {
        return new DynamoDbAnimalDao(provideDynamoDBMapper());
    }

    private DynamoDBMapper provideDynamoDBMapper() {
        return new DynamoDBMapper(DynamoDbClientProvider.provideDynamoDBClient(getRegion()));
    }

    private Regions getRegion() {
        // some logic that might return appropriate region for THIS deployment
        // of the service
    }
}

public class ZooApplication {

    public static void main(String[] args) {
        ZooService zooService = new ZooAppHelper().provideZooService();

        zooService.addNewAnimal("Ring-Tailed Lemur");
        //...
    }       
}

// ZooZervice.java
public class ZooService {
    private DynamoDbAnimalDao animalDao;

    public ZooService(DynamoDbAnimalDao animalDao) {
        this.animalDao = animalDao;    
    }

    public void addNewAnimal(String animalType) {
        //  Create and save new animal with the type passed into the method
        Animal animal = new Animal(animalType);
        animalDao.save(animal);
    }
}

// DynamoDbAnimalDao.java
public class DynamoDbAnimalDao implements AnimalDao {
    private DynamoDBMapper dynamoDbMapper;

    public DynamoDbAnimalDao(DynamoDBMapper dynamoDbMapper) {
       this.dynamoDbMapper = dynamoDbMapper;
    }

    public void save(Animal animal) {
        this.dynamoDbMapper.save(animal);
    }
}

Okay, we argue that we've made things better, but let's take stock:

  • We added more lines of code. That sounds like it might be bad, but the rest should all be good news!
  • ZooService doesn't have to create its own DynamoDbAnimalDao anymore, so the ZooService unit tests can use a mock DAO that does anything they need: throw exceptions, return garbage data, succeed, complain about duplicate records, etc. This should make us very happy.
  • DynamoDbAnimalDao doesn't have to create its own DynamoDBMapper object. DynamoDbAnimalDao unit tests can run without actually using DynamoDB at all. This is good because our unit tests shouldn't be using DynamoDB. We can recreate all sorts of DynamoDB behaviors (common and edge case), and make sure that our DAO behaves appropriately. This makes us happy.
  • We include some logic in our ZooAppHelper to figure out which region we want use when contacting DynamoDB. Maybe we want to run our tests in one region, but production in another. Or our production service runs in many different regions and we want to make sure we contact DynamoDB in the local region, wherever that region is. We don't want our DAO to have to worry about that stuff; it's great to have it live somewhere else!

It sounds like a decent improvement over the original implementation, right? This is why we've used this pattern so far in our ATA projects. And this is the basic idea behind Dependency Injection. The ZooAppHelper is "injecting" dependencies into the objects' constructors, so that they (and the objects that depend on them) don't have to do that instantiation themselves.

It's another reason dependency injection is such a helpful pattern: dependency injection separates the instantiation logic from the code that uses the instantiated object to get its work done.

More generally, we set up our classes to accept the dependency objects they need in their constructors and use dependency injection because it makes it easier to update/expand our code in the future.

Say we had a class Bank that accepted a StaffDao and VaultDao that the Bank used to manage staff and funds in the bank vault.

If we tried to update Bank to include a CashRegisterDao to manage the registers the Staff use, so they don't always have to go into the vault, we would need to update any code that instantiates a Bank to also create a CashRegisterDao.

However, if we were using a dependency injection framework like Dagger, once we updated the Bank's @Inject annotated constructor to include a CashRegisterDao, we wouldn't need to make any other changes. Dagger would take care of that for us.

Dependency injection benefits unit testing as a kind of side effect. By accepting the classes we need as arguments to our constructor, we can pass mocks in our unit tests, narrowing the scope of what our unit tests actually check to only the class we are testing.

(If you understand this, you've made the conceptual leap with us and are in good shape. If not, please re-read and ask for help. What comes next is one way to streamline the dependency injection, but the above must make sense first!)

CAUTION: Avoid mixing dependency injection and instantiating dependencies

An important thing to note is that if you are going to use Dependency Injection (and in most cases, you probably should), you need to implement dependency injection for all of your dependencies across your service.

Why do we need to implement dependency injection for all of our dependencies in our service? The primary reason is to avoid mismatched dependencies. Say, for example, that you wanted to change the implementation you use for AnimalDao from DynamoDbAnimalDao to AquaticAnimalDao, and you use AnimalDao in multiple classes in your service. If you've used dependency injection to provide this dependency, then all you need to do is update which class you instantiate in that provider method. If you created an AnimalDao outside of your dependency injection framework as well, then while the classes that use your dependency injection framework will be updated to use AquaticAnimalDao, the other classes that instantiate AnimalDao directly will still use DynamoDbAnimalDao, unless you update them as well. This can lead to mismatched dependencies across your service, which can cause unexpected side-effects. Ensuring that when you update a dependency in one place, it updates the dependency across your service, is a major benefit of using dependency injection.

Another issue that can be caused by not consistently using dependency injection across a service is the duplication of resources. In many cases, we only need to create one instance of a class. For example, all of our classes can use the same AnimalDao instance. We would gain no benefit by creating multiple instances of AnimalDao, so we want to ensure we don't create multiple instances of it. This is what we would call making a class a Singleton - we always use the same instance of the object. We can enforce this by using dependency injection, and reusing the same AnimalDao object each time we need to provide it. If a class were to not use dependency injection to obtain AnimalDao, then we would have multiple instances of AnimalDao across our service, making it no longer a Singleton, and wasting memory on the new AnimalDao instance. While that might be okay in this case, it can cause issues in other cases, such as when we use in-memory caches or implement concurrency. We will cover those topics in more detail in a later unit.

To ensure consistent behavior, make sure to implement dependency injection consistently throughout your service.

Video Content: Introducing Dagger

This video introduces Dagger, a popular dependency injection framework for Java and Android applications. Dagger uses code generation to create an efficient DI implementation at compile time.

// Define a Dagger component interface
@Component
public interface ApplicationComponent {
    // Methods to get dependencies
    UserService userService();
    
    // Method to inject dependencies into an object
    void inject(MainActivity activity);
}

// Using the component
public class MyApplication {
    private ApplicationComponent component;
    
    public void onCreate() {
        // Dagger will generate this implementation
        component = DaggerApplicationComponent.create();
        
        // Get a dependency
        UserService userService = component.userService();
    }
}

This code illustrates the basic structure of Dagger usage. The @Component annotation defines the interface between the dependency consumer and provider. Dagger generates an implementation of this interface that handles the creation and provision of the dependencies.

Using Dagger for Dependency Injection

Here we find out how we get this mysterious Dagger framework to do our dependency injection for us. We need to know what our root object is, and then tell Dagger which constructors to call on all the classes in its dependency tree.

NOTE: We use Dagger 2, in case you are searching for additional documentation/resources. The Dagger Developer's Guide has been useful for some previous participants.

Do-it-yourself dependency injection

In the previous reading, we saw one way of providing dependency injection, by implementing a separate class with methods for instantiating dependencies and providing them to the class we want to instantiate. We will soon see another approach that does something very similar, but with fewer repetitive lines of code.

Let's return to the latest iteration of our ZooApplication example:

public class ZooAppHelper {
    public ZooService provideZooService() {
        return new ZooService(provideDynamoDbAnimalDao());        
    }

    private DynamoDbAnimalDao provideDynamoDbAnimalDao() {
        return new DynamoDbAnimalDao(provideDynamoDBMapper());
    }

    private DynamoDBMapper provideDynamoDBMapper() {
        return new DynamoDBMapper(DynamoDbClientProvider.provideDynamoDBClient(getRegion()));
    }

    private Regions getRegion() {
        // some logic that might return appropriate region for THIS deployment
        // of the service
    }
}

public class ZooApplication {
    public static void main(String[] args) {
        ZooService zooService = new ZooAppHelper().provideZooService();

        zooService.addNewAnimal("Ring-Tailed Lemur");
        //...
    }       
}

// ZooZervice.java
public class ZooService {
    private DynamoDbAnimalDao animalDao;

    public ZooService(DynamoDbAnimalDao animalDao) {
        this.animalDao = animalDao;    
    }   

    public void addNewAnimal(String animalType) {
        //  Create and save new animal with the type passed into the method
        Animal animal = new Animal(animalType);
        animalDao.save(animal);
    }
}

// DynamoDbAnimalDao.java
public class DynamoDbAnimalDao {
    private DynamoDBMapper dynamoDbMapper;

    public DynamoDbAnimalDao(DynamoDBMapper dynamoDbMapper) {
       this.dynamoDbMapper = dynamoDbMapper;
    }

    public void save(Animal animal) {
        this.dynamoDbMapper.save(animal);
    }
}

But...we can do better. Why write all that repetitive logic in the ZooAppHelper? Imagine if instead of three dependencies (ZooService, DynamoDbAnimalDao, DynamoDBMapper) in the dependency chain, what if ZooApplication had 100 objects in the dependency chain? You can take a look at some of our App classes in previous units to imagine what the class starts to look like. And most of the methods tend to look the same, something like:

    public SomeObject provideSomeObject() {
        return new SomeObject(provideSomeDependency(), provideAnotherDependency());
    }

Surely we can automate some of this repetitive code away? Yes, we can. A dependency injection framework will provide shortcuts for setting up these dependency-providing methods. They will end up behaving almost exactly like our ZooAppHelper above, but with less typing, and less risk of mistakes that compile but misbehave at runtime.

A framework is a set of related classes that can help us accomplish a task more easily, or with less chance of error. One framework we have used before is JUnit, which is a unit testing framework for Java. Dagger is a dependency injection framework. Dagger allows us to specify how to provide dependency objects using annotations (rather than full methods) that are in the familiar Java annotation format, as we have seen with JUnit's @Test and DynamoDB's @DynamoDBAttribute annotations. (NOTE: when you search for Dagger documentation and examples, note that we're using the updated Dagger 2.)

Let's take a look at how we use Dagger, and we'll see that getting our root object is similar to the do-it-yourself approach above.

Dagger @Component: provider of the root object(s)

Notice that the only method in ZooAppHelper that needs to be called by our application is the provideZooService() method. All other methods are just in service of handling that method call. In fact, it's the only public method in the class; the others are all private. It is no coincidence that this is the method providing ZooService, the root object of the dependency graph. As long as that method returns a ZooService, our application is satisfied.

Wouldn't it be nice if something automatically generated all those methods, and we only had to define the root object and call provideZooService()? Enter Dagger.

Here is how our ZooApplication gets its ZooService instance with our do-it-yourself strategy:

ZooService zooService = ZooAppHelper.provideZooService();

And here is how we would do it using Dagger:

ZooAppComponent dagger = DaggerZooAppComponent.create();
ZooService zooService = dagger.provideZooService();

Here, instead of calling the provideZooService() method, we call an instance method on this special object, DaggerZooAppComponent[1]. We'll get to where that comes from in a bit, but first, let's look at the type for the variable, dagger:

@Component
public interface ZooAppComponent {
    ZooService provideZooService();
}

It's an interface where we define the Dagger root object. (After all, if we needed to implement provideZooService() ourselves, we would end up with nearly the same code as ZooAppHelper. That would defeat the purpose of using a framework.)

At build time, the Dagger framework will take the interface that we've created and generate a new class (DaggerZooAppComponent) that implements all the logic necessary to provide a ZooService instance, including creating all of ZooService's dependencies.

You probably noticed the @Component annotation. It is similar to the @Test annotation that we use to mark our unit test methods, but in this case, the annotation is applied to the entire interface, not a specific method. It tells Dagger that this is the interface that we need to create a Dagger component concrete class for. Dagger then takes care of the rest!

Here's how our service looks now with Dagger changes partially implemented:

@Component
public interface ZooAppComponent {
    ZooService provideZooService();
}

public class ZooApplication {
    public static void main(String[] args) {
        ZooAppComponent dagger = DaggerZooAppComponent.create();
        ZooService zooService = dagger.provideZooService();

        zooService.addNewAnimal("Ring-Tailed Lemur");
        //...
    }       
}

// ZooZervice.java
public class ZooService {
    private DynamoDbAnimalDao animalDao;

    public ZooService(DynamoDbAnimalDao animalDao) {
        this.animalDao = animalDao;    
    }   

    public void addNewAnimal(String animalType) {
        //  Create and save new animal with the type passed into the method
        Animal animal = new Animal(animalType);
        animalDao.save(animal);
    }
}

// DynamoDbAnimalDao.java
public class DynamoDbAnimalDao {
    private DynamoDBMapper dynamoDbMapper;

    public DynamoDbAnimalDao(DynamoDBMapper dynamoDbMapper) {
       this.dynamoDbMapper = dynamoDbMapper;
    }

    public void save(Animal animal) {
        this.dynamoDbMapper.save(animal);
    }
}

@Inject: specifying how to construct dependencies

Okay, that makes Dagger sound somewhat magic, if it can figure out how all of our objects in the dependency graph depend on one another, how to instantiate them and build them all up. Dagger actually needs a little help, by telling it which constructors to call to instantiate objects.

First, dagger will look at the return type of the provide* method(s) in the @Component interface. In our case, we just have the provideZooService() method, with return type of ZooService. Dagger can find the ZooService class, certainly, but it needs help figuring out how to instantiate one. (This is particularly relevant when the class has more than one constructor.)

Dagger requires us to annotate the specific constructor that it should use to instantiate an object of each class. The constructor can be no-arg or argumented. The annotation we use is @Inject, and it comes right before the constructor that we want Dagger to use to create the object. The @Inject annotation tells Dagger to use this constructor, and "inject" any dependency objects as arguments.

The no-arg constructors are going to be easy for Dagger to figure out what to do: just call the constructor.

Constructors with arguments seem like they would be a little trickier, but actually Dagger can handle them pretty easily as well. Dagger just looks at the type of each argument and finds an @Inject-annotated constructor for that type, calls it, and passes all constructor arguments to the constructor that requires it. It's basically "walking" down the dependency graph, finding an @Inject constructor at each step, and using that to instantiate the object needed.

Here's what our ZooService constructor would look like. Notice that the constructor itself looks exactly as it did before; all we need to add is @Inject before it.

public class ZooService {
    private DynamoDbAnimalDao animalDao;

    @Inject
    public ZooService(DynamoDbAnimalDao animalDao) {
        this.animalDao = animalDao;
    }
    //...
}

Now, when Dagger sees the provideZooService() method and its return value, ZooService, it looks at ZooService, finds the @Inject constructor, obtains the DynamoDbAnimalDao and invokes this constructor.

How does it get the DynamoDbAnimalDao? Same idea: annotate its constructor with @Inject as well:

public class DynamoDbAnimalDao {
    private DynamoDBMapper dynamoDbMapper;

    @Inject
    public DynamoDbAnimalDao(DynamoDBMapper dynamoDbMapper) {
        this.dynamoDbMapper = dynamoDbMapper;
    }
    //...
}

...And so on down the dependency graph.

The key here: any object that will be provided by Dagger will need an @Inject annotation on the constructor to be used to create an instance of that class.

Here's how our service looks now with more (but still not all) Dagger changes implemented:

@Component
public interface ZooAppComponent {
    ZooService provideZooService();
}

public class ZooApplication {
    public static void main(String[] args) {
        ZooAppComponent dagger = DaggerZooAppComponent.create();
        ZooService zooService = dagger.provideZooService();

        zooService.addNewAnimal("Ring-Tailed Lemur");
        //...
    }       
}

// ZooZervice.java
public class ZooService {
    private DynamoDbAnimalDao animalDao;

    @Inject
    public ZooService(DynamoDbAnimalDao animalDao) {
        this.animalDao = animalDao;    
    }   

    public void addNewAnimal(String animalType) {
        //  Create and save new animal with the type passed into the method
        Animal animal = new Animal(animalType);
        animalDao.save(animal);
    }
}

// DynamoDbAnimalDao.java
public class DynamoDbAnimalDao {
    private DynamoDBMapper dynamoDbMapper;

    @Inject
    public DynamoDbAnimalDao(DynamoDBMapper dynamoDbMapper) {
       this.dynamoDbMapper = dynamoDbMapper;
    }

    public void save(Animal animal) {
        this.dynamoDbMapper.save(animal);
    }
}

Will it ever end?

You may be wondering at this point about how Dagger ever actually finds all of its dependencies. If it keeps finding @Inject constructors with arguments, it has to keep finding more classes and more constructors.

The simple case is when Dagger finds a class with a no-arg constructor. Dagger can easily call the no-arg constructor (e.g. new ObjectDependency()), and pass the resulting object to the constructor that requires it.

You may have also noticed that this probably won't work for our DynamoDBMapper dependency. You're absolutely right, but we'll cover how Dagger lets you provide your own objects when necessary in the next reading! We will also see how this can let us achieve the AnimalDao polymorphism we hinted at in the previous reading!

Summary of @Component and @Inject

Believe it or not, that's most of setting up a codebase to use Dagger:

  • Create an interface, annotated with @Component
  • Declare a provide* method in the interface returning the root object that your application needs
  • Add the @Inject annotation to the constructor for that dependency class and any dependencies that it has in its dependency graph.

[1] CAUTION: If you're following along in IntelliJ, you may be having trouble with the DaggerZooAppComponent not being found. This is because Dagger hasn't generated it yet. You must successfully build one time before you can import the DaggerZooAppComponent in IntelliJ, because Dagger creates this class at build time.

Video Content: Dagger @Provides, @Module, and @Singleton

This video explores Dagger's core annotations for configuring dependency injection. @Provides methods define how to create dependencies, @Module groups these methods, and @Singleton ensures only one instance is created.

// Dagger module with provider methods
@Module
public class AppModule {
    
    // Provides a singleton database connection
    @Provides
    @Singleton
    public Database provideDatabase() {
        return new PostgresDatabase("jdbc:postgresql://localhost/mydb");
    }
    
    // Provides a UserRepository that depends on Database
    @Provides
    public UserRepository provideUserRepository(Database database) {
        return new SQLUserRepository(database);
    }
    
    // Provides UserService that depends on UserRepository
    @Provides
    public UserService provideUserService(UserRepository repository) {
        return new UserServiceImpl(repository);
    }
}

// Component with module
@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {
    UserService userService();
}

This example shows a Dagger module with provider methods. The @Singleton annotation on the Database provider ensures only one instance is created and reused. The dependency graph is clear: UserService depends on UserRepository, which depends on Database. Dagger manages this graph automatically.

Guided Project: Dependency Injection

This video provides a comprehensive overview of the Dependency Injection concepts covered in this module, bringing together all the key points from previous videos.

// Practical application - full Dagger setup
// Service layer with constructor injection
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
    private final NotificationService notificationService;
    
    @Inject // Tells Dagger to inject dependencies
    public OrderService(OrderRepository orderRepository,
                       PaymentService paymentService,
                       NotificationService notificationService) {
        this.orderRepository = orderRepository;
        this.paymentService = paymentService;
        this.notificationService = notificationService;
    }
    
    public void processOrder(Order order) {
        orderRepository.save(order);
        paymentService.processPayment(order.getPayment());
        notificationService.sendOrderConfirmation(order);
    }
}

// Component that brings everything together
@Singleton
@Component(modules = {RepositoryModule.class, ServiceModule.class})
public interface ApplicationComponent {
    OrderService orderService();
}

This comprehensive example demonstrates a complete Dagger setup with constructor injection using @Inject. The OrderService has multiple dependencies that are automatically provided by Dagger. The component interface combines multiple modules to form the complete dependency graph.

Resources