How to Implement the Dependency Injection Pattern for Loose Coupling in Java

The Dependency Injection pattern eliminates tight coupling by delegating dependency creation to external sources, enabling you to swap implementations without modifying class internals.

The Dependency Injection (DI) pattern is a cornerstone of maintainable Java applications, promoting loose coupling by ensuring classes depend on abstractions rather than concrete implementations. In the iluwatar/java-design-patterns repository, this pattern is demonstrated through a wizard example that evolves from rigid, tightly-coupled code to a flexible, framework-managed solution using Google Guice.

Understanding Dependency Injection and Inversion of Control

Inversion of Control (IoC) is the fundamental principle behind Dependency Injection. Instead of high-level modules (the wizards) instantiating low-level modules (specific tobacco types), both depend on a shared abstraction. As documented in App.java at lines 30-34, this shift ensures that neither the wizard nor the specific tobacco implementation holds control over object creation. The Wizard interface and the Tobacco abstraction form the contract that decouples the components, allowing you to substitute SecondBreakfastTobacco, RivendellTobacco, or OldTobyTobacco without altering wizard logic.

Constructor Injection for Mandatory Dependencies

Constructor injection is the preferred method for providing required dependencies, guaranteeing that an object is created in a complete, valid state. In AdvancedWizard.java, the wizard declares its dependency on the Tobacco interface through the constructor parameter, removing any responsibility for creating the concrete instance.

// Constructor injection – the wizard declares a dependency but does not create it.
public class AdvancedWizard implements Wizard {
  private final Tobacco tobacco;      // depends on abstraction
  public AdvancedWizard(Tobacco tobacco) {
    this.tobacco = tobacco;          // injected by client code
  }
  public void smoke() {
    tobacco.smoke(this);
  }
}

Because the tobacco field is declared as final, the dependency cannot be altered after construction, ensuring immutability and thread safety. The client code that instantiates AdvancedWizard maintains control over which specific tobacco implementation to provide.

Setter Injection for Flexible Configuration

Setter injection offers greater flexibility by allowing dependencies to be swapped after object creation. The AdvancedSorceress.java implementation demonstrates this approach, exposing a setter method that modifies the internal state.

// Setter injection – the dependency can be swapped later.
public class AdvancedSorceress implements Wizard {
  private Tobacco tobacco;           // mutable dependency
  public void setTobacco(Tobacco tobacco) {
    this.tobacco = tobacco;
  }
  public void smoke() {
    tobacco.smoke(this);
  }
}

This pattern suits optional dependencies or scenarios requiring runtime reconfiguration. However, you must guard against null dependencies, as the object can exist temporarily without its dependency being set.

Framework-Based Injection with Google Guice

Modern Java applications often delegate wiring entirely to a Dependency Injection framework like Google Guice. The iluwatar/java-design-patterns repository illustrates this through TobaccoModule.java and GuiceWizard.java, eliminating manual object construction.

The TobaccoModule extends Guice's AbstractModule to declaratively bind the Tobacco abstraction to a concrete implementation:

// Guice configuration – binding an abstraction to a concrete class.
public class TobaccoModule extends AbstractModule {
  @Override
  protected void configure() {
    bind(Tobacco.class).to(SecondBreakfastTobacco.class);
  }
}

The GuiceWizard receives its dependencies automatically through the framework's Injector:

// Obtaining an instance via Guice – no manual wiring needed.
Injector injector = Guice.createInjector(new TobaccoModule());
GuiceWizard wizard = injector.getInstance(GuiceWizard.class);
wizard.smoke();

This approach removes factory boilerplate and centralizes configuration in the module class, making the system easier to modify and test.

Summary

  • Dependency Injection delegates object creation to external sources, preventing classes from instantiating their own dependencies.
  • Constructor injection ensures mandatory dependencies are provided at initialization and supports immutability.
  • Setter injection allows runtime flexibility for optional dependencies that may change during the object's lifecycle.
  • Frameworks like Google Guice automate the binding of abstractions to concrete implementations via configuration modules such as TobaccoModule.java.
  • Depending on the Tobacco interface rather than concrete implementations enables swapping between SecondBreakfastTobacco, RivendellTobacco, or OldTobyTobacco without modifying wizard classes.

Frequently Asked Questions

What is the difference between Dependency Injection and Inversion of Control?

Inversion of Control is the architectural principle where control of object creation shifts from the class itself to an external container or framework. Dependency Injection is the specific implementation technique where dependencies are supplied to a class from an external source rather than the class creating them internally. According to the source code in App.java, IoC ensures that both high-level wizards and low-level tobacco implementations depend on the abstraction rather than concrete instances.

When should I use constructor injection versus setter injection?

Use constructor injection for mandatory dependencies that the object requires to function correctly, ensuring the object is always in a valid state upon creation as demonstrated in AdvancedWizard.java. Use setter injection for optional dependencies that may change during the object's lifecycle or when you need to allow post-construction configuration, as shown in AdvancedSorceress.java.

How does Google Guice improve upon manual Dependency Injection?

Google Guice automates the wiring process through binding modules like TobaccoModule.java, eliminating the need for factories or manual object construction in client code. The Guice Injector automatically resolves dependency graphs and instantiates objects such as GuiceWizard with their required dependencies already populated, reducing boilerplate while maintaining strict loose coupling.

Why is depending on abstractions important for loose coupling?

Depending on the Tobacco interface rather than concrete classes like SecondBreakfastTobacco allows you to swap implementations without modifying the wizard classes that consume them. This adherence to the Dependency Inversion Principle enables testing with mock implementations and switching production implementations based on configuration, which is the primary goal of the Dependency Injection pattern.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →