Enterprise Java

Spring Boot Dependency Injection using @Autowired

Spring Boot is a widely used framework for building production-ready Java applications with minimal configuration. At the core of Spring Boot lies the concept of Dependency Injection (DI), which is implemented using the Inversion of Control (IoC) Container. One of the most important annotations that enables DI in Spring is @Autowired. It allows the Spring container to automatically resolve and inject required dependencies into a class, removing the need for manual object creation using the new keyword. This leads to loosely coupled, testable, and maintainable code.

In real-world applications, classes often depend on multiple services or components. Instead of tightly coupling them, Spring manages their lifecycle and dependencies. The @Autowired annotation works seamlessly with other Spring features like @Component, @Service, @Repository, @Configuration, and @Bean to build a complete dependency graph at runtime. It also supports advanced concepts like multiple bean resolution using @Primary and @Qualifier, as well as different injection styles such as field, setter, and constructor injection.

Understanding @Autowired is essential for designing clean Spring Boot architectures because it directly impacts how components interact, how dependencies are resolved, and how scalable the application becomes. It also plays a key role in handling complex scenarios like circular dependencies and bean ambiguity, which are common in enterprise-level applications.

1. Autowired Annotation in Spring

@Autowired is used in Spring to automatically inject dependencies into a bean. It eliminates the need for manual object creation using new.

1.1 Types of Dependency Injection

Dependency Injection (DI) is a core principle of Spring Framework that promotes loose coupling between components. Instead of creating dependencies manually using new, Spring injects them automatically using IoC (Inversion of Control).

1.1.1 Field Injection

@Autowired
private UserService userService;

Field injection is the simplest form of dependency injection in Spring where the framework directly injects the dependency into a private field using reflection, without requiring any constructor or setter method. While it offers quick development convenience and reduces boilerplate code, it comes with significant drawbacks such as poor testability, hidden dependencies, and inability to enforce immutability since fields cannot be marked as final. Because the dependency is injected after object creation, it can also make the class harder to reason about and debug, especially in large-scale applications. Additionally, field injection tightly couples the class to the Spring container, making it less portable and less suitable for clean architecture or domain-driven design. For these reasons, it is generally discouraged in production-grade systems in favor of constructor injection, which makes dependencies explicit and ensures the object is always created in a fully initialized state.

1.2.1 Setter Injection

private UserService userService;

@Autowired
public void setUserService(UserService userService) {
    this.userService = userService;
}

Setter injection in Spring is a dependency injection technique where the framework injects the required dependency through a public setter method after the object has been constructed. This approach allows the bean to be created first and dependencies to be supplied or modified later, which makes it useful in scenarios where dependencies are optional or may need to be reconfigured dynamically during runtime. It also improves flexibility compared to field injection because dependencies are more visible through method signatures, making the class slightly easier to test and mock. However, setter injection introduces the risk of partially initialized objects since the dependency is not guaranteed to be present at construction time, which can lead to runtime NullPointerExceptions if the setter is not called by Spring or is misconfigured. Additionally, it does not support immutability since dependencies cannot be marked final, and it can make the object state mutable after creation, which is generally undesirable in modern Spring Boot applications. Despite these drawbacks, setter injection is still useful in legacy systems or when dealing with optional dependencies where strict constructor-based enforcement is not required.

1.3.1 Constructor Injection

private final UserService userService;

@Autowired
public UserController(UserService userService) {
    this.userService = userService;
}

Constructor injection is the most recommended and widely adopted dependency injection approach in modern Spring applications because it ensures that all required dependencies are provided at the time of object creation, making the class fully initialized and ready to use immediately. By enforcing dependencies through the constructor, it promotes immutability since fields can be declared as final, which enhances thread-safety and prevents accidental reassignment. This approach also improves testability significantly, as dependencies can easily be mocked and injected directly through the constructor in unit tests without relying on the Spring container. Unlike field or setter injection, constructor injection makes dependencies explicit, improving code readability and design clarity, and aligns strongly with SOLID principles, especially the Dependency Inversion Principle. Additionally, Spring framework internally resolves constructor injection at bean instantiation time, and if a class has only one constructor, the @Autowired annotation becomes optional, further simplifying the code. It also helps detect circular dependencies early during application startup, making architectural issues easier to identify and fix at design time rather than runtime.

1.2 Circular Dependencies

class A {
    @Autowired B b;
}

class B {
    @Autowired A a;
}

A circular dependency in Spring occurs when two or more beans are directly or indirectly dependent on each other, forming a dependency loop that the Spring container cannot resolve during bean creation. This typically happens when class A depends on class B, and class B simultaneously depends on class A, causing Spring to get stuck in an infinite instantiation cycle. Since Spring manages object creation through the ApplicationContext, it must fully initialize one bean before injecting it into another, but in a circular dependency scenario, neither bean can be fully created independently, leading to runtime issues such as BeanCurrentlyInCreationException or UnsatisfiedDependencyException. Circular dependencies often indicate a design flaw where responsibilities between components are tightly coupled instead of being properly separated, violating the principles of clean architecture and single responsibility. While Spring can sometimes resolve circular dependencies using setter or field injection with early bean references (proxies), constructor injection will fail immediately, which is actually beneficial because it exposes the design problem early in the development lifecycle. To fix circular dependencies, developers typically refactor the code to introduce a third service layer, extract shared logic into a separate component, or use the @Lazy annotation to delay initialization, though the latter should be treated as a workaround rather than a permanent solution. Ultimately, circular dependencies are a strong signal that the system design should be reconsidered to ensure loose coupling and better maintainability.

1.3 Common Errors and Fixes

  • NoSuchBeanDefinitionException: This error occurs when Spring is unable to find a bean definition in the application context. It usually happens when the class is not annotated with a stereotype annotation like @Service, @Component, or @Repository, or when the package containing the bean is not included in component scanning. It can also occur if Java-based configuration using @Bean is missing or not properly loaded. The fix is to ensure proper component scanning using @SpringBootApplication or @ComponentScan, and verify that the bean is correctly defined and registered in the Spring context.
  • NullPointerException: This typically occurs when a dependency is expected but not injected by Spring, resulting in a null reference at runtime. The most common causes include using manual object creation with new instead of relying on Spring, incorrect use of @Autowired, or misconfigured beans that are not part of the application context. Another subtle cause is using field injection in classes that are instantiated outside Spring management. The fix is to ensure the class is managed by Spring, dependencies are properly annotated and scanned, and constructor injection is used wherever possible to guarantee initialization at object creation time.
  • Multiple Beans Found (NoUniqueBeanDefinitionException): This error occurs when multiple implementations of the same interface exist in the Spring context and Spring cannot decide which one to inject. For example, if there are multiple PaymentService implementations, Spring throws ambiguity during autowiring. The resolution is to use @Qualifier to explicitly specify which bean should be injected, or @Primary to define a default bean that Spring should prefer when multiple candidates are available. Proper use of these annotations ensures deterministic dependency resolution and avoids runtime ambiguity issues.

2. Code Example

2.1 Main Application

package com.example.demo;

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

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

The Main Application class is the entry point of the Spring Boot application. It is annotated with @SpringBootApplication, which enables component scanning, auto-configuration, and configuration support in a single annotation. The main method uses SpringApplication.run() to bootstrap the application, starting the embedded server (like Tomcat) and initializing the Spring context. This class essentially triggers the entire Spring Boot lifecycle, where all beans, services, controllers, and configurations are detected and managed automatically by the Spring container.

2.2 Repository Interface

package com.example.demo.repo;

public interface PaymentRepository {
    String pay();
}

The PaymentRepository interface defines a contract for payment-related operations in the application. It declares a single method pay() which must be implemented by any concrete class. In Spring Boot, interfaces like this are commonly used to support loose coupling, allowing multiple implementations (such as card payment, UPI, or wallet) to be swapped easily without changing the service layer. This design also enables Spring’s dependency injection to dynamically inject the appropriate implementation at runtime.

2.3 Two Implementations (@Primary + @Component)

package com.example.demo.repo;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
@Primary
public class CardPaymentRepository implements PaymentRepository {

    @Override
    public String pay() {
        return "Paid using CARD (Primary Bean)";
    }
}

The CardPaymentRepository class is one of the implementations of the PaymentRepository interface and is marked with @Component so that Spring automatically detects and registers it as a bean during component scanning. The @Primary annotation makes this implementation the default choice when multiple beans of the same type exist, meaning Spring will prefer this class for dependency injection unless another bean is explicitly selected using @Qualifier. The pay() method provides a concrete implementation that returns a message indicating that the payment was processed using a card, demonstrating how different implementations can coexist under a common interface while one is designated as the default.

package com.example.demo.repo;

import org.springframework.stereotype.Component;

@Component("upiRepo")
public class UpiPaymentRepository implements PaymentRepository {

    @Override
    public String pay() {
        return "Paid using UPI (Qualifier Bean)";
    }
}

The UpiPaymentRepository class is another implementation of the PaymentRepository interface and is annotated with @Component("upiRepo"), which registers it as a Spring bean with an explicit name upiRepo. This name becomes important when resolving ambiguity between multiple beans of the same type, allowing it to be selected using @Qualifier. Unlike the @Primary bean, this implementation is not chosen by default. The pay() method returns a message indicating that the payment was processed using UPI, demonstrating how Spring allows multiple interchangeable implementations while still giving control over which one is injected at runtime.

2.4 Bean using @Configuration + @Bean

package com.example.demo.config;

import com.example.demo.repo.PaymentRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean("walletRepo")
    public PaymentRepository walletPaymentRepository() {
        return new PaymentRepository() {
            @Override
            public String pay() {
                return "Paid using WALLET (Bean Config)";
            }
        };
    }
}

The AppConfig class is a Spring configuration class annotated with @Configuration, which indicates that it contains bean definitions that will be managed by the Spring container. Inside it, the walletPaymentRepository() method is annotated with @Bean("walletRepo"), meaning Spring will register the returned object as a bean with the name walletRepo. Instead of using a class annotated with @Component, this approach manually defines a bean using an anonymous implementation of the PaymentRepository interface. This is useful when you need fine-grained control over bean creation or when integrating third-party classes that cannot be modified. The pay() method here returns a message indicating a wallet-based payment, demonstrating another interchangeable implementation managed directly through configuration.

2.5 Service Layer (@Autowired + @Qualifier)

package com.example.demo.service;

import com.example.demo.repo.PaymentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    private final PaymentRepository paymentRepository;

    @Autowired
    public PaymentService(@Qualifier("upiRepo") PaymentRepository paymentRepository) {
        this.paymentRepository = paymentRepository;
    }

    public String makePayment() {
        return paymentRepository.pay();
    }
}

The PaymentService class represents the service layer in the application and is annotated with @Service, making it a Spring-managed bean responsible for business logic. It depends on the PaymentRepository interface, which is injected via constructor-based dependency injection using @Autowired. The @Qualifier("upiRepo") annotation explicitly tells Spring to inject the UpiPaymentRepository implementation instead of relying on the default @Primary bean, resolving ambiguity when multiple implementations exist. The dependency is stored in a final field, ensuring immutability after construction. The makePayment() method delegates the call to the injected repository’s pay() method, demonstrating how the service layer remains decoupled from concrete implementations while still executing business logic.

2.6 Controller

package com.example.demo.controller;

import com.example.demo.service.PaymentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PaymentController {

    @Autowired
    private PaymentService paymentService;

    @GetMapping("/pay")
    public String pay() {
        return paymentService.makePayment();
    }
}

The PaymentController class is a REST controller in Spring Boot, annotated with @RestController, which combines @Controller and @ResponseBody to directly return responses as JSON or plain text. It depends on the PaymentService, which is injected using @Autowired into the field, allowing Spring to automatically wire the service bean at runtime. The @GetMapping("/pay") annotation maps HTTP GET requests on the /pay endpoint to the pay() method. When this endpoint is called, the controller delegates the request to paymentService.makePayment(), and the result is returned as the HTTP response. This demonstrates the standard Spring Boot flow where the controller handles incoming requests, the service layer contains business logic, and the repository layer handles data or implementation details.

2.7 Code Run and Output

When the Spring Boot application is started, the embedded server (such as Tomcat) initializes and all Spring-managed beans are created based on annotations like @Component, @Service, @Configuration, and @Bean. The dependency injection mechanism resolves the PaymentService dependency inside PaymentController, and within the service layer, the @Qualifier("upiRepo") ensures that the UpiPaymentRepository implementation is injected instead of the default @Primary bean. When a client sends a GET request to /pay, the request flows from the controller to the service layer, which in turn calls the repository implementation. As a result, the application returns the response: Paid using UPI (Qualifier Bean), demonstrating how Spring Boot dynamically wires dependencies and executes the correct implementation at runtime.

3. Conclusion

Integrating Spring Boot with a SPA requires proper handling of unknown routes to avoid 404 errors. Redirecting all unmatched paths to index.html ensures smooth client-side routing. Additionally, understanding @Autowired, dependency injection types, and bean management techniques like @Qualifier, @Primary, and @Configuration is essential for building scalable Spring applications. Proper dependency design helps avoid circular dependencies, improves testability, and makes the application more maintainable.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button