What is Dependency Injection & How is it Achieved in Spring Boot?

java dev.to

Learn what Dependency Injection is, why it matters in Spring Boot, and how to implement it using Java 21 with practical examples, REST APIs, and best practices.

What is Dependency Injection & How is it Achieved in Spring Boot?

Introduction

Imagine you're building a house.

You need electricity, plumbing, internet, and furniture. Now imagine if the house itself had to create all these things from scratch. It would become extremely complex and difficult to maintain.

Instead, specialized providers supply these services, and the house simply uses them.

Dependency Injection (DI) works in a similar way.

In traditional Java programming, a class often creates the objects it depends on using the new keyword. This creates tight coupling, making the application harder to test, maintain, and extend.

With Dependency Injection, the required objects (dependencies) are provided to a class from the outside instead of being created inside it.

This concept is at the heart of Spring Boot and is one of the primary reasons why Spring Boot applications are flexible, scalable, and easy to test.

In this article, you'll learn:

  • What Dependency Injection is
  • Why it is important
  • How Spring Boot implements it
  • Practical Java 21 examples
  • Best practices and common mistakes

Core Concepts

What is Dependency Injection?

Dependency Injection is a design pattern where an object's dependencies are supplied externally rather than being created by the object itself.

Without Dependency Injection

public class OrderService {

    private PaymentService paymentService = new PaymentService();

}
Enter fullscreen mode Exit fullscreen mode

The OrderService is responsible for creating PaymentService.

Problems:

  • Tight coupling
  • Difficult unit testing
  • Hard to replace implementations
  • Violates the Dependency Inversion Principle

With Dependency Injection

public class OrderService {

    private final PaymentService paymentService;

    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now the dependency is provided from outside.

Benefits:

  • Loose coupling
  • Better testability
  • Easier maintenance
  • More flexible architecture

How Spring Boot Achieves Dependency Injection

Spring Boot uses the Spring IoC (Inversion of Control) Container.

The container:

  1. Creates objects (Beans)
  2. Manages their lifecycle
  3. Injects dependencies automatically

Common annotations used:

Annotation Purpose
@Component Generic Spring Bean
@Service Service layer Bean
@Repository Data access Bean
@Controller MVC Controller
@RestController REST API Controller
@Autowired Inject dependency
@Configuration Bean configuration
@Bean Creates custom Bean

Types of Dependency Injection in Spring Boot

1. Constructor Injection (Recommended)

public UserService(UserRepository repository) {
    this.repository = repository;
}
Enter fullscreen mode Exit fullscreen mode

Advantages:

  • Immutable dependencies
  • Easier testing
  • Clear design
  • Recommended by Spring Team

2. Setter Injection

public void setRepository(UserRepository repository) {
    this.repository = repository;
}
Enter fullscreen mode Exit fullscreen mode

Useful for optional dependencies.

3. Field Injection

@Autowired
private UserRepository repository;
Enter fullscreen mode Exit fullscreen mode

Not recommended because:

  • Harder to test
  • Hidden dependencies
  • Reduced immutability

Use Cases of Dependency Injection

Dependency Injection is commonly used in:

REST APIs

Inject services into controllers.

Database Access

Inject repositories into services.

Messaging Systems

Inject Kafka or RabbitMQ producers.

Payment Gateways

Switch between different payment providers easily.

Unit Testing

Replace real objects with mocks.

Code Example 1: Constructor Dependency Injection in Spring Boot

This example demonstrates the recommended approach.

Project Structure

src/main/java
|
+-- controller
|   +-- GreetingController.java
|
+-- service
|   +-- GreetingService.java
|
+-- SpringBootDiApplication.java
Enter fullscreen mode Exit fullscreen mode

GreetingService.java

package com.example.demo.service;

import org.springframework.stereotype.Service;

@Service
public class GreetingService {

    public String greet() {
        return "Hello from Dependency Injection!";
    }
}
Enter fullscreen mode Exit fullscreen mode

GreetingController.java

package com.example.demo.controller;

import com.example.demo.service.GreetingService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingController {

    private final GreetingService greetingService;

    // Constructor Injection
    public GreetingController(GreetingService greetingService) {
        this.greetingService = greetingService;
    }

    @GetMapping("/api/greeting")
    public String greeting() {
        return greetingService.greet();
    }
}
Enter fullscreen mode Exit fullscreen mode

Application Class

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootDiApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootDiApplication.class, args);
    }
}
Enter fullscreen mode Exit fullscreen mode

Run the Application

mvn spring-boot:run
Enter fullscreen mode Exit fullscreen mode

Request

curl -X GET http://localhost:8080/api/greeting
Enter fullscreen mode Exit fullscreen mode

Response

Hello from Dependency Injection!
Enter fullscreen mode Exit fullscreen mode

Code Example 2: Dependency Injection with Interface-Based Design

This is a real-world approach that demonstrates loose coupling.

PaymentProcessor.java

package com.example.demo.payment;

public interface PaymentProcessor {

    String processPayment(double amount);
}
Enter fullscreen mode Exit fullscreen mode

CreditCardPaymentProcessor.java

package com.example.demo.payment;

import org.springframework.stereotype.Service;

@Service
public class CreditCardPaymentProcessor implements PaymentProcessor {

    @Override
    public String processPayment(double amount) {
        return "Payment of $" + amount + " processed via Credit Card";
    }
}
Enter fullscreen mode Exit fullscreen mode

PaymentService.java

package com.example.demo.service;

import com.example.demo.payment.PaymentProcessor;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    private final PaymentProcessor paymentProcessor;

    // Constructor Injection
    public PaymentService(PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }

    public String pay(double amount) {
        return paymentProcessor.processPayment(amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

PaymentController.java

package com.example.demo.controller;

import com.example.demo.service.PaymentService;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/payments")
public class PaymentController {

    private final PaymentService paymentService;

    public PaymentController(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    @PostMapping("/{amount}")
    public String processPayment(@PathVariable double amount) {
        return paymentService.pay(amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

Request

curl -X POST http://localhost:8080/api/payments/500
Enter fullscreen mode Exit fullscreen mode

Response

Payment of $500.0 processed via Credit Card
Enter fullscreen mode Exit fullscreen mode

Why This Design Is Better

Suppose tomorrow you want to use:

  • PayPal
  • Stripe
  • Razorpay
  • Bank Transfer

You only create another implementation of PaymentProcessor.

The service layer remains unchanged.

This is the true power of Dependency Injection.

Benefits of Dependency Injection

1. Loose Coupling

Classes depend on abstractions rather than implementations.

2. Easier Unit Testing

Mock dependencies easily.

PaymentProcessor mockProcessor = mock(PaymentProcessor.class);
Enter fullscreen mode Exit fullscreen mode

3. Better Maintainability

Changes in one component rarely affect others.

4. Improved Scalability

Applications become easier to extend.

5. Cleaner Architecture

Follows SOLID principles and enterprise development standards.

Best Practices

1. Prefer Constructor Injection

Constructor Injection is the Spring-recommended approach because dependencies are mandatory and immutable.

2. Depend on Interfaces

Use:

PaymentProcessor processor;
Enter fullscreen mode Exit fullscreen mode

instead of:

CreditCardPaymentProcessor processor;
Enter fullscreen mode Exit fullscreen mode

This promotes loose coupling.

3. Avoid Field Injection

Avoid:

@Autowired
private PaymentService service;
Enter fullscreen mode Exit fullscreen mode

It makes testing more difficult.

4. Keep Services Focused

A service should have a single responsibility.

Avoid creating large service classes that perform many unrelated tasks.

5. Let Spring Manage Beans

Do not manually instantiate Spring-managed components.

Avoid:

PaymentService service = new PaymentService();
Enter fullscreen mode Exit fullscreen mode

Instead, let Spring inject the dependency.

Common Mistakes

  • Using new for Spring-managed classes
  • Overusing @Autowired on fields
  • Creating circular dependencies
  • Injecting concrete implementations everywhere
  • Ignoring interface-based design

Conclusion

Dependency Injection is one of the most important concepts in Spring Boot and modern Java programming.

Instead of creating dependencies manually, Spring's IoC container creates and injects them automatically. This results in:

  • Cleaner code
  • Better testability
  • Loose coupling
  • Easier maintenance
  • More scalable applications

When working with Spring Boot, always prefer Constructor Dependency Injection and design your application around interfaces. These practices help you build production-ready applications that are easy to extend and maintain.

As you continue to learn Java and Spring Boot, mastering Dependency Injection will significantly improve your software design skills.

Call to Action

Have you used Dependency Injection in your Spring Boot projects?

Share your experience, questions, or challenges in the comments below. If you're learning Spring Boot and Java programming, feel free to ask questions—I'd be happy to help!

Source: dev.to

arrow_back Back to Tutorials