CodeGym /Courses /Module 5. Spring /Core Principles of Dependency Injection (DI)

Core Principles of Dependency Injection (DI)

Module 5. Spring
Level 2 , Lesson 1
Available

Dependency Injection (dependency injection) sounds fancy, but it's just a trendy name for passing one object (a dependency) into another object. Put simply, DI is the way an object (let's call it the consumer) gets all the dependencies it needs to do its job.

Think of a restaurant. Cooking dishes is the chef's responsibility. Of course, the chef won't run to the market hunting for the best veggies, nor will they raise cows for the beef steak. Other specialists handle that, and the restaurant provides the chef with the ingredients. In programming it's similar: an object shouldn't create its own dependencies — that should be done from the outside.

Dependency injection (DI) makes your code:

  1. Flexible: if you need to swap a dependency (for example, switch from one database to another), you can do it without rewriting all the business logic.
  2. Testable: you can easily replace dependencies with stubs/mocks for testing.
  3. Easy to maintain: changes in one dependency minimally affect the rest of the code.

The problem with the traditional approach

Let's look at an example without DI:


public class OrderService {
    private final PaymentService paymentService;

    public OrderService() {
        this.paymentService = new PaymentService(); // We create the dependency instance ourselves
    }

    public void placeOrder() {
        paymentService.processPayment();
    }
}

What's wrong here? Well, at least:

  1. OrderService depends on a concrete implementation of PaymentService. If we need to use a different PaymentService, we'd have to change the code.
  2. Testing OrderService becomes harder because we can't replace PaymentService with a stub.

How DI fixes it

DI solves this by passing dependencies from the outside:


public class OrderService {
    private final PaymentService paymentService;

    // Dependency is passed in from outside via the constructor
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public void placeOrder() {
        paymentService.processPayment();
    }
}

Now we can pass any implementation of PaymentService (for example, CreditCardPaymentService or PayPalPaymentService), which makes the code flexible.


Benefits of using DI

Testability

With DI you can easily replace real dependencies with stubs/mocks for testing. For example:


PaymentService mockPaymentService = Mockito.mock(PaymentService.class);
OrderService orderService = new OrderService(mockPaymentService);

Maintainability

When objects don't create their own dependencies, it's easier to swap them later. For example, if you need to add a new implementation of PaymentService, you can do it without changing OrderService.

Scalability

DI helps scale applications. Imagine if instead of 10 classes you had 1000, and all of them manually created their own dependencies. Managing that turns into a nightmare. DI handles this easily and frees you from dependency management chores.


Ways to inject dependencies

Spring Framework supports three main DI approaches:

  1. Constructor-based
  2. Setter-based
  3. Field-based

We'll dig into them in the next lecture, but here's a quick overview.

Constructor injection

With this approach dependencies are passed through the constructor parameters:


@Component
public class OrderService {
    private final PaymentService paymentService;

    @Autowired
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public void placeOrder() {
        paymentService.processPayment();
    }
}

In most cases it's best to use this approach because it:

  • Makes dependencies required to initialize (the compiler helps enforce the constructor).
  • Allows creating immutable dependencies (final).

Setter injection

Dependencies are passed via setters. This makes them optional:


@Component
public class OrderService {
    private PaymentService paymentService;

    @Autowired
    public void setPaymentService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public void placeOrder() {
        paymentService.processPayment();
    }
}

This approach is better for default configurations or situations where dependencies aren't always required.

Field injection

This way uses the @Autowired annotation directly on the field. While it's the most concise, it's less preferred because it breaks encapsulation:


@Component
public class OrderService {
    @Autowired
    private PaymentService paymentService;

    public void placeOrder() {
        paymentService.processPayment();
    }
}

DI in Spring Framework

So, Spring Framework gives us tools to use DI. That's great — we no longer have to manage dependencies by hand. The main DI principles in Spring:

Automatic dependency wiring (Autowired)

The @Autowired annotation is used to automatically inject required dependencies. Spring figures out the appropriate dependency by type.

Example:


@Component
public class OrderService {
    private final PaymentService paymentService;

    @Autowired
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

Spring will automatically find a bean of type PaymentService and pass it into the constructor.

Java-based configuration with the @Bean annotation

You can also define dependencies manually using configuration classes:


@Configuration
public class AppConfig {

    @Bean
    public PaymentService paymentService() {
        return new PayPalPaymentService();
    }

    @Bean
    public OrderService orderService(PaymentService paymentService) {
        return new OrderService(paymentService);
    }
}

DI in real projects

We could repeat this one more time, but you probably already know DI helps a lot in web apps, microservices, and testing.

  1. Microservices: in microservices DI is used to inject services, repositories, configurations, and API clients.
  2. Web applications: DI lets you keep a single instance of a service that's used by all controllers to handle requests.
  3. Testing: DI makes it easy to swap real dependencies with mock objects when writing tests.

Real example

If you're building a REST API to handle orders, DI makes it easy to wire controllers, services, and repositories:


@RestController
@RequestMapping("/orders")
public class OrderController {

    private final OrderService orderService;

    @Autowired
    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping
    public ResponseEntity<String> createOrder() {
        orderService.placeOrder();
        return ResponseEntity.ok("Order placed successfully!");
    }
}

Here OrderController is totally agnostic — it doesn't care how OrderService is implemented; it just uses it.


Common mistakes and how to fix them

Circular dependencies

If two beans reference each other, Spring won't be able to inject them and will throw an error. For example:


@Component
public class A {
    @Autowired
    private B b;
}

@Component
public class B {
    @Autowired
    private A a;
}

To fix this, it's better to rethink the app architecture and make dependencies one-way.

Ambiguous dependencies

If you have multiple beans of the same type, Spring doesn't know which one to inject:


@Component
public class CreditCardPaymentService implements PaymentService {}

@Component
public class PayPalPaymentService implements PaymentService {}

@Component
public class OrderService {
    @Autowired
    private PaymentService paymentService; // Error: which bean to choose?
}

The solution is to use the @Qualifier annotation:


@Component
public class OrderService {

    @Autowired
    @Qualifier("creditCardPaymentService")
    private PaymentService paymentService;
}

Now that you understand what DI is, why it's useful, and how to apply it, we're ready to dive into the different ways to inject dependencies so you can pick the right tool for each situation.

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION