@Autowired Annotation in Spring Boot

Spring Boot @Autowired connects the classes together without tight coupling, making application modular, testable, and easy to maintain.

Spring Boot

Before Spring, Java developers managed dependencies manually by creating objects in the dependent class itself. For example, if OrderService needed a PaymentService, we will write:

public class OrderService {
    private PaymentService paymentService = new PaymentService();
}

This looks harmless at first, but it creates serious problems:

  • OrderService is tightly coupled to PaymentService i.e you can’t swap it out
  • You can’t easily unit test OrderService in isolation
  • If PaymentService itself needs other objects, you have to chain new calls everywhere
  • Object creation is scattered across your entire codebase with no central control

Spring solves this with the IoC (Inversion of Control) principle. Instead of the code creating its own dependencies, we invert the responsibility and let the framework handle it. @Autowired is the annotation that tells Spring, “I need this dependency and you figure out how to provide it.”

This simple shift in thinking is what makes large Spring Boot applications manageable, testable, and maintainable.

1. The Spring IoC Container – The Engine Behind @Autowired

To truly understand @Autowired, we need to understand what happens when a Spring Boot application starts.

@SpringBootApplication
public class MyApp {
    public static void main(String[] args) {

        SpringApplication.run(MyApp.class, args);
    }
}

When you run the main method, Spring performs the following steps internally:

  • Spring scans your packages for classes annotated with stereotype annotations like @Component, @Service, @Repository, @Controller, and @Configuration. Every class it finds is a candidate to become a bean.
  • Each discovered class is registered in the ApplicationContext which is Spring’s central registry of all managed objects. Think of it as a smart HashMap where the key is the bean type (or name) and the value is the actual object instance.
  • Spring reads every @Autowired annotation it finds across all beans. For each one, it looks into the ApplicationContext to find a matching bean by type. If found, it injects it. If not found, it throws NoSuchBeanDefinitionException at startup which is actually a good thing, because you catch the problem immediately rather than at runtime.
  • Spring manages the full lifecycle i.e. creation, initialization (via @PostConstruct), usage, and destruction (via @PreDestroy). You never call new and you never call destroy.
  • This entire mechanism – scanning, registering, resolving, injecting – is what makes @Autowired work.

2. Field Injection – Simple but Problematic

Field injection is the most concise form, and the one beginner reach for first:

@Service
public class ProductService {

    @Autowired
    private CategoryRepository categoryRepository;

    @Autowired
    private InventoryService inventoryService;

    public Product getProductDetails(Long id) {
        // use both dependencies
        return new Product();
    }
}

Spring uses Java Reflection to inject values directly into private fields, bypassing normal access rules. This is why you don’t need a constructor or setter – Spring reaches directly into the field.

2.1. Why this is problematic in production code?

First, it makes unit testing painful. To test ProductService, you need to spin up a Spring context or use ugly reflection hacks to set private fields. With constructor injection, you just pass a mock object.

Second, field injection hides your dependencies. Looking at the class signature, you can’t immediately tell what it needs. With constructor injection, every dependency is declared right there in the constructor parameters – the class’s contract is explicit.

Third, it allows classes to grow unchecked. When adding a new dependency is as easy as typing @Autowired on a new field, there’s no friction stopping you. Classes gradually accumulate 10, 12, 15 dependencies without anyone noticing. A constructor with 10 parameters is an obvious warning sign. 10 @Autowired fields are easy to ignore.

Fourth, final fields cannot use field injection. You lose immutability guarantees.

Use field injection in test classes (@SpringBootTest) where convenience outweighs these concerns, but avoid it in production code.

3. Constructor Injection – The Right Way

Constructor injection is the approach officially recommended by the Spring team and by the broader Java community:

@Service
public class OrderService {

    private final PaymentService paymentService;
    private final InventoryService inventoryService;
    private final NotificationService notificationService;

    @Autowired
    public OrderService(
            PaymentService paymentService,
            InventoryService inventoryService,
            NotificationService notificationService) {

        this.paymentService = paymentService;
        this.inventoryService = inventoryService;
        this.notificationService = notificationService;
    }

    public OrderConfirmation placeOrder(Order order) {

        inventoryService.reserve(order);
        paymentService.charge(order);
        notificationService.sendConfirmation(order);
        return new OrderConfirmation(order.getId());
    }
}

Notice that all three fields are final. This is a critical advantage – once the constructor runs, the dependencies are set in stone. No one can accidentally call a setter and replace them mid-execution. The object is in a fully initialized, immutable state from the moment it’s created.

Also notice: since Spring 4.3, if a class has exactly one constructor, the @Autowired annotation is optional. Spring assumes that single constructor is the one to use. This means in modern Spring Boot, you often see:

@Service
public class OrderService {

    private final PaymentService paymentService;

    // No @Autowired needed — Spring infers it
    public OrderService(PaymentService paymentService) {

        this.paymentService = paymentService;
    }
}

And with Lombok, you can eliminate even the constructor boilerplate:

@Service
@RequiredArgsConstructor  // Lombok generates constructor for all final fields
public class OrderService {

    private final PaymentService paymentService;
    private final InventoryService inventoryService;
    private final NotificationService notificationService;

    public OrderConfirmation placeOrder(Order order) {
        // ...
    }
}

@RequiredArgsConstructor generates a constructor with all final fields as parameters. Spring Boot picks it up automatically. This is the cleanest pattern in modern Spring Boot development.

4. Setter Injection – For Truly Optional Dependencies

Setter injection sits in the middle ground – more flexible than field injection, but less strict than constructor injection:

@Service
public class ReportService {

    private final DataFetcher dataFetcher;
    private CacheService cacheService;   // optional — not final

    @Autowired
    public ReportService(DataFetcher dataFetcher) {
        this.dataFetcher = dataFetcher;
    }

    @Autowired(required = false)
    public void setCacheService(CacheService cacheService) {
        this.cacheService = cacheService;
    }

    public Report generateReport(String type) {
        List<Data> data = dataFetcher.fetch(type);

        if (cacheService != null) {
            cacheService.store(type, data);
        }

        return new Report(data);
    }
}

Here, DataFetcher is required (constructor injection, final field) while CacheService is optional (setter injection, required = false). The service works correctly whether or not caching is available. This is the legitimate use case for setter injection – optional enhancements that don’t affect core functionality.

5. @Qualifier – For Resolving Ambiguity

When Spring finds multiple beans of the same type, it doesn’t know which one to inject and throws NoUniqueBeanDefinitionException. This is a common scenario when working with interfaces:

public interface PaymentGateway {
    PaymentResult process(Payment payment);
}

@Service
public class StripeGateway implements PaymentGateway {

    @Override
    public PaymentResult process(Payment payment) {

        System.out.println("Processing via Stripe...");
        return new PaymentResult("stripe-txn-001");
    }
}

@Service
public class PayPalGateway implements PaymentGateway {

    @Override
    public PaymentResult process(Payment payment) {

        System.out.println("Processing via PayPal...");
        return new PaymentResult("paypal-txn-001");
    }
}

Spring sees two beans for PaymentGateway. Without guidance, it fails. Fix it with @Qualifier:

@Service
public class CheckoutService {

    private final PaymentGateway paymentGateway;

    @Autowired
    public CheckoutService(@Qualifier("stripeGateway") PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }

    public void checkout(Cart cart) {
        paymentGateway.process(cart.toPayment());
    }
}

The qualifier name "stripeGateway" matches the default bean name Spring assigns which is the class name with a lowercase first letter. You can also set a custom name:

@Service("stripe")
public class StripeGateway implements PaymentGateway { ... }

// Then use:
@Qualifier("stripe")

6. @Primary – Another Alternative

If one implementation should be the default choice, mark it with @Primary. Other beans can still override it with @Qualifier:

@Service
@Primary  // This is the default PaymentGateway
public class StripeGateway implements PaymentGateway { ... }

@Service
public class PayPalGateway implements PaymentGateway { ... }

// This gets StripeGateway automatically (no @Qualifier needed)
@Autowired
private PaymentGateway paymentGateway;

// This explicitly gets PayPalGateway
@Autowired
@Qualifier("payPalGateway")
private PaymentGateway paymentGateway;

7. Injecting Collections of Beans

One of @Autowired‘s most powerful and underused features is injecting all implementations of an interface at once. Spring will gather every registered bean of the matching type and inject them all into a List or Map.

public interface DataValidator {
    ValidationResult validate(Order order);
    String getName();
}

@Component
public class StockValidator implements DataValidator {

    public ValidationResult validate(Order order) {

        boolean inStock = checkStock(order);
        return new ValidationResult("StockValidator", inStock, "Item out of stock");
    }
    public String getName() { return "StockValidator"; }
    private boolean checkStock(Order order) { return true; } // simplified
}

@Component
public class FraudValidator implements DataValidator {

    public ValidationResult validate(Order order) {
        boolean notFraud = checkFraud(order);
        return new ValidationResult("FraudValidator", notFraud, "Suspicious activity detected");
    }

    public String getName() { return "FraudValidator"; }
    private boolean checkFraud(Order order) { return true; } // simplified
}

@Component
public class AddressValidator implements DataValidator {

    public ValidationResult validate(Order order) {
        boolean validAddress = checkAddress(order);
        return new ValidationResult("AddressValidator", validAddress, "Invalid delivery address");
    }

    public String getName() { return "AddressValidator"; }
    private boolean checkAddress(Order order) { return true; } // simplified
}

// Spring injects all three automatically
@Service
public class OrderValidationService {

    private final List<DataValidator> validators;

    @Autowired
    public OrderValidationService(List<DataValidator> validators) {
        this.validators = validators;
    }

    public void validate(Order order) {
        for (DataValidator validator : validators) {
            ValidationResult result = validator.validate(order);
            if (!result.isPassed()) {
                throw new ValidationException(result.getMessage());
            }
        }
        System.out.println("All " + validators.size() + " validators passed.");
    }
}

Now when you add a new validator – say PaymentMethodValidator – you just create a new @Component class. Zero changes to OrderValidationService. This is the Open/Closed Principle in action: open for extension, closed for modification.

You can also inject as a Map<String, DataValidator> where the key is the bean name:

@Autowired
private Map<String, DataValidator> validatorMap;
// Keys: "stockValidator", "fraudValidator", "addressValidator"

8. Circular Dependencies – Understanding and Fixing Them

A circular dependency occurs when Bean A depends on Bean B, and Bean B depends on Bean A. With constructor injection, Spring detects this at startup and throws BeanCurrentlyInCreationException which is actually helpful. It forces you to fix a design problem.

// THIS WILL FAIL AT STARTUP with constructor injection
@Service
public class UserService {
    private final RoleService roleService;

    public UserService(RoleService roleService) {
        this.roleService = roleService;
    }
}

@Service
public class RoleService {
    private final UserService userService;

    public RoleService(UserService userService) {
        this.userService = userService;
    }
}

Why this is a design problem: If A truly needs B and B truly needs A, both classes are doing too much. They’re overlapping in responsibility. The fix is to look at what methods each class calls on the other, and extract that shared logic into a third class:

// Extract shared logic into a dedicated service
@Service
public class UserRoleAssignmentService {
    // Contains the methods that both UserService and RoleService needed from each other
    public void assignRole(User user, Role role) { ... }
    public void revokeRole(User user, Role role) { ... }
}

@Service
public class UserService {
    private final UserRoleAssignmentService assignmentService;

    public UserService(UserRoleAssignmentService assignmentService) {
        this.assignmentService = assignmentService;
    }
}

@Service
public class RoleService {
    private final UserRoleAssignmentService assignmentService;

    public RoleService(UserRoleAssignmentService assignmentService) {
        this.assignmentService = assignmentService;
    }
}

No more circular dependency. Both services depend on a shared third service, and responsibilities are clearly separated.

9. @Autowired with @Configuration and @Bean

Sometimes you define beans manually in a @Configuration class instead of using @Component. @Autowired works here too:

@Configuration
public class AppConfig {

    @Bean
    public RestTemplate restTemplate() {
        RestTemplate template = new RestTemplate();
        template.setConnectTimeout(Duration.ofSeconds(5));
        return template;
    }

    @Bean
    public EmailClient emailClient() {
        return new EmailClient("smtp.example.com", 587);
    }
}

// Now inject these beans anywhere
@Service
public class ExternalApiService {

    private final RestTemplate restTemplate;
    private final EmailClient emailClient;

    @Autowired
    public ExternalApiService(RestTemplate restTemplate, EmailClient emailClient) {
        this.restTemplate = restTemplate;
        this.emailClient = emailClient;
    }

    public String callApi(String url) {
        return restTemplate.getForObject(url, String.class);
    }
}

Beans defined with @Bean in @Configuration classes are first-class citizens in the ApplicationContext and @Autowired finds and injects them exactly like @Component beans.

10. @Autowired in Spring Boot Tests

In integration tests with @SpringBootTest, you can autowire beans directly into your test class:

@SpringBootTest
class OrderServiceIntegrationTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void shouldSaveOrderAndSendConfirmation() {
        Order order = new Order(1L, "LAPTOP", 2);
        OrderConfirmation confirmation = orderService.placeOrder(order);

        assertNotNull(confirmation.getOrderId());
        assertTrue(orderRepository.existsById(confirmation.getOrderId()));
    }
}

For pure unit tests, don’t use @SpringBootTest at all. Just use constructor injection with Mockito:

@ExtendWith(MockitoExtension.class)
class OrderServiceUnitTest {

    @Mock
    private PaymentService paymentService;

    @Mock
    private InventoryService inventoryService;

    @InjectMocks
    private OrderService orderService;
    // Mockito calls the constructor and injects the mocks

    @Test
    void shouldProcessPaymentWhenOrderPlaced() {
        Order order = new Order(1L, "LAPTOP", 1);
        orderService.placeOrder(order);

        verify(paymentService, times(1)).charge(order);
        verify(inventoryService, times(1)).reserve(order);
    }
}

@InjectMocks works perfectly with constructor injection – Mockito finds the constructor and injects the @Mock objects. No Spring context, no slow startup, pure fast unit testing.

11. Common Errors and How to Fix Them

  • NoSuchBeanDefinitionException: Spring can’t find a bean of the required type.
    • Causes: missing @Service/@Component annotation, the class is in a package not scanned by Spring, or a typo in the class name.
    • Fix: add the stereotype annotation and ensure the package is under your main application class.
  • NoUniqueBeanDefinitionException: Spring found multiple beans matching the type.
    • Fix: use @Qualifier to specify which one, or mark the preferred implementation with @Primary.
  • BeanCurrentlyInCreationException: You have a circular dependency.
    • Fix: redesign to extract shared logic into a third service. As a last resort, use @Lazy on one injection point, but treat that as a temporary patch, not a solution.
  • UnsatisfiedDependencyException: A wrapper exception that usually wraps one of the above. Read the root cause in the stack trace and it will point to the exact bean and field causing the problem.

12. Conclusion

@Autowired is the foundation of dependency injection in Spring Boot. It connects your classes together without tight coupling, making your application modular, testable, and easy to maintain. The annotation itself is simple but understanding the IoC container behind it, the different injection types, qualifier resolution, and the design principles that make it work well separates a beginner from a confident Spring Boot developer.

The golden rule: constructor injection, final fields, interface-based design. Everything else follows naturally from there.

Happy Learning !!

Comments

Subscribe
Notify of
0 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.