Spring – [ IOC ]

Inversion of Control (IoC) means : You don’t create dependencies – you let the Spring Container create them for you and inject them where needed.

Instead of your code calling the dependency, the container gives your code the dependency.

1. Quick Start With A Simple Example

Imagine you’re building a small user management application.

You need a UserService to handle user logic, and it needs a UserRepository to save data.

You might write :

package com.example2;

public class WithoutIoC {
    public static void main(String[] args) {
        UserRepository userRepository = new UserRepository();
        UserService userService = new UserService(userRepository);

        userService.addUser("Tom");
        userService.addUser("Jerry");
    }

    private static class UserService {
        private final UserRepository repo;

        public UserService(UserRepository repo) {
            this.repo = repo;
        }

        public void addUser(String name) {
            this.repo.save(name);
        }
    }

    private static class UserRepository {
        public void save(String name) {
            System.out.println("User " + name + " saved to repository.");
        }
    }
}
/*
 * Output:
 * User Tom saved to repository.
 * User Jerry saved to repository.
 */

Now with Spring, you can write :

package com.example2;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

@Configuration
public class WithIoC {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context =
            new AnnotationConfigApplicationContext(WithIoC.class);

        UserService userService = context.getBean(UserService.class);

        userService.addUser("Tom");
        userService.addUser("Jerry");

        context.close();
    }

    @Service
    private static class UserService {
        private final UserRepository repo;

        public UserService(UserRepository repo) {
            this.repo = repo;
        }

        public void addUser(String name) {
            this.repo.save(name);
        }
    }

    @Component
    private static class UserRepository {
        public void save(String name) {
            System.out.println("User " + name + " saved to repository.");
        }
    }
}
/*
 * Output:
 * User Tom saved to repository.
 * User Jerry saved to repository.
 */

Here, we never use new to create an instance of UserService, so its constructor is never used directly.

Spring creates an instance of UserService for us.

This kind of instance created by Spring are called Bean.

Spring also creates an UserRepository bean, then inject it to UserService bean.

This kind of behavior is called Dependency Injection (DI), which is a core principle of IoC.

To sum up :

Without IoC :

  • You —–> new UserRepository() —–> new UserService()

With IoC :

  • Spring Container —–> UserService Bean —–> UserRepository Bean —–> inject UserRepository bean to UserService bean
  • You —–> get UserService Bean from Spring Container

2. Inversion of Control (IoC) and Dependency Injection (DI)

IoC isn’t a technology, but rather a philosophy, a key principle of object-oriented programming that guides how to design loosely coupled, better-performing programs.

Traditional applications rely on us actively creating dependent objects within classes, resulting in high coupling between classes and making testing difficult.

With an IoC container, control over creating and finding dependent objects is transferred to the container, which then injects the composite objects.

This results in loose coupling between objects, facilitating testing and functionality reuse.

More importantly, it makes the entire program architecture extremely flexible.

In fact, the biggest change IoC brings to programming isn’t in the code itself, but in the mindset : a shift in the master-slave paradigm.

Originally, the application was the boss, proactively acquiring resources.

However, with IoC, the application becomes passive, passively waiting for the IoC container to create and inject the resources it needs.

IoC is a good example of the Hollywood principle: Don’t look for us, we will look for you.

That is, the IoC container helps the object find the corresponding dependent object and inject it, rather than the object actively looking for it.

Inversion of Control is achieved through Dependency Injection.

In fact, they are two different perspectives of the same concept.

In layman’s terms, IoC is a design concept and DI is an implementation method.

3. IoC Configurations in Spring

3.1 XML Based Configuration

It is to configure the bean definitions in a .xml file and Spring creates the beans for us by loading the file.

The XML Configuration :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="gs" class="com.example2.IocXmlBased.GreetingService">
        <constructor-arg value="Hello World!" />
    </bean>

</beans>

The Bean Class and Main App :

package com.example2;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class IocXmlBased {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");

        GreetingService service = ctx.getBean("gs", GreetingService.class);

        service.greet();

        ctx.close();
    }

    private static class GreetingService {
        private String message;

        public GreetingService(String message) {
            this.message = message;
        }

        public void greet() {
            System.out.println(message);
        }
    }
}
/*
 * Output:
 * Hello World!
 */

Advantages of XML Configuration :

  • Keeps configuration outside of Java code;
  • All bean definitions can be in one place, easy to read at a glance;
  • Works with very old Spring versions and older Java (pre-Java 5, where annotations were rare).

Disadvantages of XML Configuration :

  • Renaming a class or method in code won’t automatically update XML references and no compile-time checking for bean IDs or property names – errors appear only at runtime;
  • With many XML files, finding a bean definition is slower than clicking to a Java class;
  • Weaker integration with modern Spring features, Spring Boot and newer libraries are optimized for annotation & Java config.

When Xml Based Configuration is useful :

  • You’re maintaining legacy applications built on XML config;
  • You want to configure beans from external sources without touching Java code;
  • You work in environments where Java recompilation is expensive or restricted;
  • You want absolutely explicit control over bean definitions with no classpath scanning.

3.2 Annotation Based Configuration

By adding annotations to the class, you can declare a bean to be managed by Spring.

It covers two related but different styles :

  • Java Configuration : Uses @Configuration classes to define beans and their dependencies using methods annotated with @Bean.
package com.example2;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class IocAnnotationBased {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                IocAnnotationBased.class);

        GreetingService service = ctx.getBean(GreetingService.class);

        service.greet();

        ctx.close();
    }

    @Bean
    public GreetingService greetingService() {
        return new GreetingService("Hello World!");
    }

    private static class GreetingService {
        private String message;

        public GreetingService(String message) {
            this.message = message;
        }

        public void greet() {
            System.out.println(message);
        }
    }
}
/*
 * Output :
 * Hello World!
 */
  • Component Scanning : Automatically detects classes annotated with Stereotype Annotations like @Component, @Service, @Repository, or @Controller and registers them as beans.
package com.example3;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Service;

import jakarta.annotation.PostConstruct;

@ComponentScan
public class IocAnnotationBased3 {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                IocAnnotationBased3.class);

        GreetingService service = ctx.getBean(GreetingService.class);

        service.greet();

        ctx.close();
    }

    @Service
    private static class GreetingService {
        private String message;

        public void greet() {
            System.out.println(message);
        }

        @PostConstruct
        public void init() {
            this.message = "Hello World!";
        }
    }
}
/*
 * Output :
 * Hello World!
 */

Advantages of Annotation Based Configuration :

  • Bean definitions live close to the code that uses them, adding a new bean is often as simple as writing a class with @Component or a method with @Bean, annotations like @Conditional, @Profile, and Java logic inside @Bean methods allow flexible setup;
  • Refactoring tools in IntelliJ/Eclipse can update annotations automatically when class/method names change;
  • Most modern Spring Boot projects rely heavily on annotation-based config.

Disadvantages of Annotation Based Configuration :

  • Bean definitions can be spread across many classes – harder to get a full picture of the app’s wiring;
  • Component scanning may register beans you didn’t expect (especially when using @ComponentScan with a wide base package);
  • Your business classes contain Spring annotations, making them less portable to other frameworks;
  • Unlike XML, we can’t easily change the configuration without touching Java code.

When Annotation Based Configuration is useful :

  • You’re starting a new project and want fast development with minimal boilerplate;
  • You’re using Spring Boot or any modern Spring setup;
  • Your team is mostly Java developers comfortable with annotations;
  • You want type-safety and IDE refactoring support.

3.3 Programmatic Configuration

It is to register beans programmatically in the code.

package com.example2;

import org.springframework.context.support.GenericApplicationContext;

public class IocJavaBased {
    public static void main(String[] args) {
        GenericApplicationContext ctx = new GenericApplicationContext();

        ctx.registerBean(GreetingService.class, () -> new GreetingService("Hello World!"));

        ctx.refresh();

        GreetingService service = ctx.getBean(GreetingService.class);

        service.greet();

        ctx.close();
    }

    private static class GreetingService {
        private String message;

        public GreetingService(String message) {
            this.message = message;
        }

        public void greet() {
            System.out.println(message);
        }
    }
}
/*
 * Output :
 * Hello World!
 */

Advantages of Programmatic Configuration :

  • Keeps configuration pure Java, you can use any Java logic (conditions, loops, environment checks) when defining beans;
  • You can create contexts and register beans on the fly in tests or in dynamic plugin systems;
  • No dependency on spring annotations or xml files.

Disadvantages of Programmatic Configuration :

  • You need to manually register each bean and you must explicitly pass dependencies when registering beans – it can become tedious and can lead to fat configuration classes if you have many beans;
  • Many Spring developers expect XML or annotation-based config, so this style may be less familiar.

When Programmatic Configuration is useful :

  • You’re writing unit tests or integration tests where you only need a small set of beans;
  • You need dynamic bean registration at runtime (e.g. plugins, runtime module loading);
  • You’re embedding Spring inside another system (desktop apps, game engines, IoT apps) where annotations or XML aren’t practical.

3.4 Comparison of Spring IoC Configurations

Feature / AspectXML Configuration@Component Scanning@Configuration + @BeanProgrammatic Configuration
Bean Definition LocationExternal .xml filesInside Java classes with @Component, @Service, etc.Inside @Configuration classes using @Bean methodsIn plain Java code using registerBean(), registerSingleton(), etc.
Wiring Style<bean> tags with constructor-arg / <property>Auto-detected via classpath scanningExplicit Java method return values as beansExplicit Java code registering beans
Spring Dependency in Business CodeNoYes (annotations in business classes)Yes (annotations in config classes, but not in business classes)No (unless you use Spring-specific types)
Type SafetyNo (runtime errors if bean IDs/properties are wrong)Yes (compile-time safety in Java code)YesYes
Refactoring SupportWeak (IDE won’t auto-update XML IDs)Strong (renaming classes updates references)StrongStrong
Configuration LocationCentralized in XML filesScattered across annotated classesCentralized in config classesCentralized in setup code
BoilerplateHighLowMediumMedium-High
Runtime FlexibilityHigh (change XML without recompiling)Low (requires recompilation)Low (requires recompilation)Medium-High (can create beans at runtime)
Best ForLegacy projects, externalized configModern apps, quick setupFine-grained bean creation, 3rd-party beansDynamic contexts, tests, embedded Spring
Example<bean id=”myService” class=”com.example.MyService”/>@Component class MyService {}@Bean MyService myService() { return new MyService(); }context.registerBean(MyService.class);

4. Different ways of Dependency Injection

There are 3 main common dependency injection ways :

  • Constructor Injection
  • Setter Injection
  • Field Injection

There is also Lookup Method Injection which is rare and is an advanced use case.

4.1 Constructor Injection

Dependencies are provided through the class constructor.

  • In Xml Based Configuration :
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- Define PaymentService bean -->
    <bean id="paymentService" class="com.example2.DIConstructorXML.PaymentService" />

    <!-- Define OrderService bean with constructor injection -->
    <bean id="orderService" class="com.example2.DIConstructorXML.OrderService">
        <constructor-arg ref="paymentService" />
    </bean>
</beans>
package com.example2;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class DIConstructorXML {
    private static class PaymentService {
        public void processPayment() {
            System.out.println("Processing payment...");
        }
    }

    private static class OrderService {
        private final PaymentService paymentService;

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

        public void placeOrder() {
            System.out.println("Placing order...");
            paymentService.processPayment();
        }
    }

    public static void main(String[] args) {
        try (ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans2.xml")) {
            OrderService orderService = context.getBean(OrderService.class);
            orderService.placeOrder();
        }
    }
}
/*
 * Output :
 * Placing order...
 * Processing payment...
 */
  • In Annotation Based Configuration :
package com.example3;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Service;

@ComponentScan
public class DIConstructorAnnotation {
    @Service
    private static class PaymentService {
        public void processPayment() {
            System.out.println("Processing payment...");
        }
    }

    @Service
    private static class OrderService {
        private final PaymentService paymentService;

        @Autowired // Optional if only one constructor
        public OrderService(PaymentService paymentService) {
            this.paymentService = paymentService;
        }

        public void placeOrder() {
            System.out.println("Placing order...");
            paymentService.processPayment();
        }
    }

    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DIConstructorAnnotation.class)) {
            OrderService orderService = context.getBean(OrderService.class);
            orderService.placeOrder();
        }
    }
}
/*
 * Output :
 * Placing order...
 * Processing payment...
 */

4.2 Setter Injection

Dependencies are injected via public setter methods after object creation.

  • In Xml Based Configuration :
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- Define PaymentService bean -->
    <bean id="paymentService" class="com.example2.DISetterXML.PaymentService" />

    <!-- Define OrderService bean with setter injection -->
    <bean id="orderService" class="com.example2.DISetterXML.OrderService">
        <property name="paymentService" ref="paymentService" />
    </bean>
</beans>
package com.example2;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class DISetterXML {
    private static class PaymentService {
        public void processPayment() {
            System.out.println("Processing payment...");
        }
    }

    private static class OrderService {
        private PaymentService paymentService;

        // Setter method
        public void setPaymentService(PaymentService paymentService) {
            this.paymentService = paymentService;
        }

        public void placeOrder() {
            System.out.println("Placing order...");
            paymentService.processPayment();
        }
    }

    public static void main(String[] args) {
        try (ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans3.xml")) {
            OrderService orderService = context.getBean(OrderService.class);
            orderService.placeOrder();
        }
    }
}
/*
 * Output:
 * Placing order...
 * Processing payment...
 */
  • In Annotation Based Configuration :
package com.example4;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Service;

@ComponentScan
public class DISetterAnnotation {
    @Service
    private static class PaymentService {
        public void processPayment() {
            System.out.println("Processing payment...");
        }
    }

    @Service
    private static class OrderService {
        private PaymentService paymentService;

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

        public void placeOrder() {
            System.out.println("Placing order...");
            paymentService.processPayment();
        }
    }

    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
                DISetterAnnotation.class)) {
            OrderService orderService = context.getBean(OrderService.class);
            orderService.placeOrder();
        }
    }
}
/*
 * Output:
 * Placing order...
 * Processing payment...
 */

4.3 Field Injection

Dependencies are injected directly into the fields of the class.

package com.example5;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Service;

@ComponentScan
public class DIFieldInjection {
    @Service
    private static class PaymentService {
        public void processPayment() {
            System.out.println("Processing payment...");
        }
    }

    @Service
    private static class OrderService {

        // Field injection
        @Autowired
        private PaymentService paymentService;

        public void placeOrder() {
            System.out.println("Placing order...");
            paymentService.processPayment();
        }
    }

    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DIFieldInjection.class)) {
            OrderService orderService = context.getBean(OrderService.class);
            orderService.placeOrder();
        }
    }
}
/*
 * Output:
 * Placing order...
 * Processing payment...
 */

4.4 Lookup Method Injection

A method is overridden by Spring at runtime to provide a bean when needed.

It’s used when you want a singleton bean to get a new prototype bean each time it needs it.

  • In Xml Based Configuration :
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           https://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- Prototype bean -->
    <bean id="paymentService" class="com.example2.DILookupXML.PaymentService" scope="prototype"/>

    <!-- Singleton bean with lookup-method injection -->
    <bean id="orderService" class="com.example2.DILookupXML.OrderService">
        <lookup-method name="getPaymentService" bean="paymentService"/>
    </bean>

</beans>
package com.example2;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class DILookupXML {
    private static class PaymentService {
        public PaymentService() {
            System.out.println("New PaymentService created: " + this);
        }

        public void processPayment() {
            System.out.println("Processing payment...");
        }
    }

    private static abstract class OrderService {
        public OrderService() {}
        
        // abstract method for lookup
        protected abstract PaymentService getPaymentService();

        public void placeOrder() {
            System.out.println("Placing order...");
            PaymentService paymentService = getPaymentService();
            paymentService.processPayment();
        }
    }

    public static void main(String[] args) {
        try (ClassPathXmlApplicationContext context =
                     new ClassPathXmlApplicationContext("beans4.xml")) {

            OrderService orderService = context.getBean(OrderService.class);

            orderService.placeOrder();
            orderService.placeOrder();
            orderService.placeOrder();
        }
    }
}
/*
 * Output:
 * Placing order...
 * New PaymentService created: com.example2.DILookupXML$PaymentService@2b662a77
 * Processing payment...
 * Placing order...
 * New PaymentService created: com.example2.DILookupXML$PaymentService@7f0eb4b4
 * Processing payment...
 * Placing order...
 * New PaymentService created: com.example2.DILookupXML$PaymentService@5c33f1a9
 * Processing payment...
 */
  • In Annotation Based Configuration :
package com.example6;

import org.springframework.beans.factory.annotation.Lookup;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

@ComponentScan
public class DILookupAnnotation {
    @Service
    @Scope("prototype")
    private static class PaymentService {
        public PaymentService() {
            System.out.println("New PaymentService created: " + this);
        }

        public void processPayment() {
            System.out.println("Processing payment...");
        }
    }

    @Service
    private static abstract class OrderService {
        public OrderService() {}

        // Abstract lookup method – Spring will override this
        @Lookup
        protected abstract PaymentService getPaymentService();

        public void placeOrder() {
            System.out.println("Placing order...");
            PaymentService paymentService = getPaymentService(); // always new instance
            paymentService.processPayment();
        }
    }

    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DILookupAnnotation.class)) {
            OrderService orderService = context.getBean(OrderService.class);

            orderService.placeOrder();
            orderService.placeOrder();
            orderService.placeOrder();
        }
    }
}
/*
 * Output:
 * Placing order...
 * New PaymentService created: com.example6.DILookupAnnotation$PaymentService@69fb6037
 * Processing payment...
 * Placing order...
 * New PaymentService created: com.example6.DILookupAnnotation$PaymentService@36d585c
 * Processing payment...
 * Placing order...
 * New PaymentService created: com.example6.DILookupAnnotation$PaymentService@87a85e1
 * Processing payment...
 */

4.5 ObjectFactory / Provider Injection

It is one of the modern ways in Spring to solve the problem of getting a new instance of a prototype bean inside a singleton bean.

Instead of relying on @Lookup injection, you inject a factory object (ObjectFactory<T> or Provider<T>) that gives you a new instance when you ask for it.

ObjectFactory<T> (Spring-specific) :

It is an interface provided by Spring that allows you to get a new instance of a bean when you call its getObject() method.

package com.example7;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@ComponentScan
public class DIObjectFactory {

    @Component
    private static class OrderService {

        private final ObjectFactory<PaymentService> paymentServiceFactory;

        @Autowired
        public OrderService(ObjectFactory<PaymentService> paymentServiceFactory) {
            this.paymentServiceFactory = paymentServiceFactory;
        }

        public void placeOrder() {
            PaymentService paymentService = paymentServiceFactory.getObject(); // fresh instance each time
            paymentService.processPayment();
        }
    }

    @Component
    @Scope("prototype") // Prototype scope
    private static class PaymentService {
        public void processPayment() {
            System.out.println("Processing payment with instance: " + this);
        }
    }

    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
                DIObjectFactory.class)) {
            OrderService orderService = context.getBean(OrderService.class);

            orderService.placeOrder();
            orderService.placeOrder();
            orderService.placeOrder();
        }
    }
}
/*
 * Output:
 * Processing payment with instance: com.example7.DIObjectFactory$PaymentService@72d1ad2e
 * Processing payment with instance: com.example7.DIObjectFactory$PaymentService@399f45b1
 * Processing payment with instance: com.example7.DIObjectFactory$PaymentService@478190fc
 */

Provider<T> (JSR-330 / Jakarta Inject standard) :

It comes from javax.inject.Provider (or jakarta.inject.Provider) and works the same way as ObjectFactory.

It is preferred if you want to stay framework-agnostic (not Spring-specific).

package com.example8;

import jakarta.inject.Provider;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@ComponentScan
public class DIProvider {

    @Component
    private static class OrderService {

        private final Provider<PaymentService> paymentServiceProvider;

        public OrderService(Provider<PaymentService> paymentServiceProvider) {
            this.paymentServiceProvider = paymentServiceProvider;
        }

        public void placeOrder() {
            PaymentService paymentService = paymentServiceProvider.get(); // fresh instance
            paymentService.processPayment();
        }
    }

    @Component
    @Scope("prototype") // Prototype scope
    private static class PaymentService {
        public void processPayment() {
            System.out.println("Processing payment with instance: " + this);
        }
    }

    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
                DIProvider.class)) {
            OrderService orderService = context.getBean(OrderService.class);

            orderService.placeOrder();
            orderService.placeOrder();
            orderService.placeOrder();
        }
    }
}
/*
 * Output:
 * Processing payment with instance: com.example8.DIProvider$PaymentService@5158b42f
 * Processing payment with instance: com.example8.DIProvider$PaymentService@595b007d
 * Processing payment with instance: com.example8.DIProvider$PaymentService@72d1ad2e
 */

4.6 Comparison of Dependency Injection in Spring

Injection StyleHow it WorksAdvantagesDisadvantagesTypical Use Case
Constructor InjectionDependencies provided via constructor.Ensures all dependencies are provided at object creation; good for immutability.Can lead to complex constructors if many dependencies; harder to refactor.Recommended for mandatory dependencies, especially in service classes.
Setter InjectionDependencies provided via setter methods.Allows changing dependencies after object creation; easier to refactor.Can lead to partially constructed objects if setters aren’t called; less suitable for mandatory dependencies.Useful for optional dependencies or when you need to change dependencies dynamically.
Field Injections@Autowired directly on fields.Simplifies code; no need for constructor or setter methods.Makes testing harder; tightly couples class to Spring framework; less clear dependencies.Quick prototyping, small classes, or when you want to avoid boilerplate code, but not recommended for production code.
Lookup Method InjectionSpring overrides an abstract method to fetch beans (often prototype) at runtime.Allows singleton beans to get new instances of prototype beans; useful for complex scenarios.More complex to set up; less readable; not commonly used.Advanced use cases where you need a new instance of a prototype bean in a singleton context.
Provider / ObjectFactory InjectionInjects a factory (ObjectFactory<T> or Provider<T>) that can be called to get a bean when needed.Decouples bean creation from injection; allows getting new instances of prototype beans in singleton contexts.Requires additional interfaces; can be less readable.Modern applications needing dynamic bean creation without tight coupling to Spring.

5. Different Annotations for Dependency Injection

5.1 @Autowired

When Spring sees :

@Autowired

private PaymentService paymentService;

It tries to inject a bean of type PaymentService into that field.

The resolution happens in the following order :

  • Type-Based Lookup :

Spring first looks in the ApplicationContext for a bean whose type matches PaymentService, if only one bean of that type exists, it’s injected automatically.

  • If multiple beans of the same type exist, for example :

@Component

public class PaypalPaymentService extends PaymentService {}

@Component

public class CreditCardPaymentService extends PaymentService {}

Now Spring sees two beans of type PaymentService, it needs extra hints :

Option A : Mark one bean as the default by @Primary, then Spring will choose this one.

package com.example9;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@ComponentScan
public class AutowiredWithPrimaryExample {
    private static class PaymentService {
        protected void processPayment() {
            System.out.println("Processing payment with instance: " + this);
        }
    }

    @Component
    private static class PaypalPaymentService extends PaymentService {
    }

    @Component
    @Primary
    private static class CreditCardPaymentService extends PaymentService {
    }

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

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

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
                AutowiredWithPrimaryExample.class);
        OrderService service = context.getBean(OrderService.class);
        service.placeOrder();
        context.close();
    }
}
/*
 * Output:
 * Processing payment with instance: com.example9.AutowiredWithPrimaryExample$CreditCardPaymentService@47af7f3d
 */

Option B : Tell Spring exactly which bean you want by @Qualifier.

package com.example10;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;

@ComponentScan
public class AutowiredWithQualifierExample {
private static class PaymentService {
        protected void processPayment() {
            System.out.println("Processing payment with instance: " + this);
        }
    }

    @Component("PayPal")
    private static class PaypalPaymentService extends PaymentService {
    }

    @Component("CreditCard")
    private static class CreditCardPaymentService extends PaymentService {
    }

    @Component
    private static class OrderService {
        @Autowired
        @Qualifier("CreditCard")
        private PaymentService paymentService;

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

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
                AutowiredWithQualifierExample.class);
        OrderService service = context.getBean(OrderService.class);
        service.placeOrder();
        context.close();
    }
}
/*
 * Output:
 * Processing payment with instance: com.example10.AutowiredWithQualifierExample$CreditCardPaymentService@158da8e
 */
  • Name-Based FallBack :

If Spring can’t resolve by type, it tries to match by field name.

@Autowired

private PaymentService paymentService;

If there’s a bean named paymentService (the default name for @Component class PaymentService), Spring will inject that one.

package com.example11;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;

@ComponentScan
public class AutowiredFallBackExample {
    @Component("paymentService")
    private static class PaymentService {
        protected void processPayment() {
            System.out.println("Processing payment with instance: " + this);
        }
    }

    @Component
    private static class PaypalPaymentService extends PaymentService {
    }

    @Component
    private static class CreditCardPaymentService extends PaymentService {
    }

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

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

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
                AutowiredFallBackExample.class);
        OrderService service = context.getBean(OrderService.class);
        service.placeOrder();
        context.close();
    }
}
/*
 * Output:
 * Processing payment with instance: com.example11.AutowiredFallBackExample$PaymentService@3ce1e309
 */
  • NoSuchBeanDefinitionException :

If there is no corresponding bean, a NoSuchBeanDefinitionException is thrown.

package com.example12;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;

@ComponentScan
public class OptionalInjectionExample {
    // @Component
    private static class PaymentService {
        protected void processPayment() {
            System.out.println("Processing payment with instance: " + this);
        }
    }

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

        public void placeOrder() {
            if (paymentService != null) {
                paymentService.processPayment();
            } else {
                System.out.println("No payment service available, proceeding without payment.");
            }
        }
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
                OptionalInjectionExample.class);
        OrderService service = context.getBean(OrderService.class);
        service.placeOrder();
        context.close();
    }
}
/*
 * Output:
 * Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'optionalInjectionExample.OrderService': Unsatisfied dependency expressed through field 'paymentService': No qualifying bean of type 'com.example12.OptionalInjectionExample$PaymentService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:772)
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:752)
        at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:145)
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:493)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1420)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:523)
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325)
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:973)
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:946)
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:616)
        at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:93)
        at com.example12.OptionalInjectionExample.main(OptionalInjectionExample.java:32)
    Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example12.OptionalInjectionExample$PaymentService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1878)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1404)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1348)
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:769)
 */

To avoid this, you can set @Autowired(required = false) to make the injection optional.

package com.example12;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;

@ComponentScan
public class OptionalInjectionExample {
    // @Component
    private static class PaymentService {
        protected void processPayment() {
            System.out.println("Processing payment with instance: " + this);
        }
    }

    @Component
    private static class OrderService {
        @Autowired(required = false)
        private PaymentService paymentService;

        public void placeOrder() {
            if (paymentService != null) {
                paymentService.processPayment();
            } else {
                System.out.println("No payment service available, proceeding without payment.");
            }
        }
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
                OptionalInjectionExample.class);
        OrderService service = context.getBean(OrderService.class);
        service.placeOrder();
        context.close();
    }
}
/*
 * Output:
 * No payment service available, proceeding without payment.
 */

Or use @Nullable (from org.springframework.lang.Nullable) to allow null injection.

package com.example13;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;

@ComponentScan
public class NullableInjectionExample {
    // @Component
    private static class PaymentService {
        protected void processPayment() {
            System.out.println("Processing payment with instance: " + this);
        }
    }

    @Component
    private static class OrderService {
        @Autowired
        @Nullable
        private PaymentService paymentService;

        public void placeOrder() {
            if (paymentService != null) {
                paymentService.processPayment();
            } else {
                System.out.println("No payment service available, proceeding without payment.");
            }
        }
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
                NullableInjectionExample.class);
        OrderService service = context.getBean(OrderService.class);
        service.placeOrder();
        context.close();
    }
}
/*
 * Output:
 * No payment service available, proceeding without payment.
 */

Or use Optional<T> to represent an optional dependency.

package com.example14;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;

@ComponentScan
public class AlternativeOptionalExample {
    // @Component
    private static class PaymentService {
        protected void processPayment() {
            System.out.println("Processing payment with instance: " + this);
        }
    }

    @Component
    private static class OrderService {
        private PaymentService paymentService;

        @Autowired
        public OrderService(Optional<PaymentService> paymentService) {
            this.paymentService = paymentService.orElse(null);
        }

        public void placeOrder() {
            if (paymentService != null) {
                paymentService.processPayment();
            } else {
                System.out.println("No payment service available, proceeding without payment.");
            }
        }
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
                AlternativeOptionalExample.class);
        OrderService service = context.getBean(OrderService.class);
        service.placeOrder();
        context.close();
    }
}
/*
 * Output:
 * No payment service available, proceeding without payment.
 */

5.2 @Inject

The @Inject annotation is part of the JSR-330 (Jakarta Dependency Injection) standard, which is a specification for dependency injection in Java.

It is similar to Spring’s @Autowired but has some differences in features and behavior.

Key Differences between @Inject and @Autowired :

  • Required Attribute :

@Inject does not have a required attribute like @Autowired(required = false).

If no matching bean is found, it will throw a NoSuchBeanDefinitionException by default.

To make an injection optional with @Inject, you can use it in combination with javax.inject.Provider<T> or java.util.Optional<T>.

  • Qualifiers :

Both @Inject and @Autowired support qualifiers to disambiguate between multiple beans of the same type.

However, @Inject uses the @Named annotation from JSR-330 for this purpose, while @Autowired uses Spring’s @Qualifier.

package com.example15;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;

import jakarta.inject.Inject;
import jakarta.inject.Named;

@ComponentScan
public class InjectExample {
    private static class PaymentService {
        protected void processPayment() {
            System.out.println("Processing payment with instance: " + this);
        }
    }

    @Component("paypalPaymentService")
    private static class PaypalPaymentService extends PaymentService {
    }

    @Component("creditCardPaymentService")
    private static class CreditCardPaymentService extends PaymentService {
    }

    @Component
    private static class OrderService {
        @Inject
        @Named("creditCardPaymentService")
        private PaymentService paymentService;

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

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
                InjectExample.class);
        OrderService service = context.getBean(OrderService.class);
        service.placeOrder();
        context.close();
    }
}
/*
 * Output:
 *  Processing payment with instance: com.example15.InjectExample$CreditCardPaymentService@2f4948e4
 */
  • Primary Beans :

@Autowired supports the @Primary annotation to indicate a preferred bean when multiple candidates are available.

@Inject does not have a direct equivalent to @Primary.

You would need to use @Named or other mechanisms to specify which bean to inject.

  • Framework Dependency :

Using @Inject makes your code more portable across different dependency injection frameworks that support JSR-330.

However, it may lack some of the advanced features provided by Spring’s @Autowired.

In summary, @Inject is a standard annotation that promotes portability, while @Autowired offers more features and flexibility specific to the Spring framework.

If you want to write framework-agnostic code, use @Inject, if you need Spring-specific features, use @Autowired.

Both can be used effectively in Spring applications, and Spring supports both annotations.

Spring’s support for JSR-330 means you can choose the annotation that best fits your needs.

In practice, many developers use @Autowired for its additional features, but @Inject is a good choice for those prioritizing standardization and portability.

Both annotations can coexist in the same project, allowing you to leverage the strengths of each where appropriate.

Overall, the choice between @Inject and @Autowired often comes down to personal or team preference, as well as specific project requirements.

5.3 @Resource

The @Resource annotation is part of the JSR-250 specification and is used for dependency injection in Java.

It is different from Spring’s @Autowired and JSR-330’s @Inject in terms of how it resolves dependencies.

Key Characteristics of @Resource :

  • Name-Based Injection :

@Resource primarily resolves dependencies by the name of the field or setter method it is applied to.

If a bean with the same name exists in the ApplicationContext, it will be injected.

For example, if you have a field named paymentService, @Resource will look for a bean named paymentService.

package com.example17;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;

import jakarta.annotation.Resource;

@ComponentScan
public class ResourceNameBasedExample {
    @Component("paymentService")
    private static class PaymentService {
        protected void processPayment() {
            System.out.println("Processing payment with instance: " + this);
        }
    }

    @Component
    private static class PaypalPaymentService extends PaymentService {
    }

    @Component
    private static class CreditCardPaymentService extends PaymentService {
    }

    @Component
    private static class OrderService {
        @Resource
        private PaymentService paymentService;

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

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
                ResourceNameBasedExample.class);
        OrderService service = context.getBean(OrderService.class);
        service.placeOrder();
        context.close();
    }
}
/*
 * Output:
 *  Processing payment with instance: com.example17.ResourceNameBasedExample$PaymentService@5b12b668
 */

If you want to specify a different bean name, you can use the name attribute of @Resource.

package com.example16;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;

import jakarta.annotation.Resource;

@ComponentScan
public class ResourceExample {
    private static class PaymentService {
        protected void processPayment() {
            System.out.println("Processing payment with instance: " + this);
        }
    }

    @Component("paypalPaymentService")
    private static class PaypalPaymentService extends PaymentService {
    }

    @Component("creditCardPaymentService")
    private static class CreditCardPaymentService extends PaymentService {
    }

    @Component
    private static class OrderService {
        @Resource(name = "paypalPaymentService")
        private PaymentService paymentService;

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

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
                ResourceExample.class);
        OrderService service = context.getBean(OrderService.class);
        service.placeOrder();
        context.close();
    }
}
/*
 * Output:
 *  Processing payment with instance: com.example16.ResourceExample$PaypalPaymentService@327514f
 */
  • Type-Based Fallback :

If no bean with the matching name is found, @Resource will fall back to type-based resolution, similar to @Autowired.

If exactly one bean of the required type exists, it will be injected.

package com.example18;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;

import jakarta.annotation.Resource;

@ComponentScan
public class ResourceTypeBasedExample {
    private static class PaymentService {
        protected void processPayment() {
            System.out.println("Processing payment with instance: " + this);
        }
    }

    @Component
    private static class CreditCardPaymentService extends PaymentService {
    }

    @Component
    private static class OrderService {
        @Resource
        private PaymentService paymentService;

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

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
                ResourceTypeBasedExample.class);
        OrderService service = context.getBean(OrderService.class);
        service.placeOrder();
        context.close();
    }
}
/*
 * Output:
 *  Processing payment with instance: com.example18.ResourceTypeBasedExample$CreditCardPaymentService@1f2586d6
 */
  • No Required Attribute :

@Resource does not have a required attribute like @Autowired(required = false).

If no matching bean is found, it will throw a NoSuchBeanDefinitionException by default.

To make an injection optional with @Resource, you can use it in combination with javax.annotation.Nullable or java.util.Optional<T>.

  • Qualifiers :

@Resource does not support qualifiers like @Autowired and @Inject.

To disambiguate between multiple beans of the same type, you would need to rely on bean names or other mechanisms.

  • Framework Dependency :

Using @Resource makes your code less portable across different dependency injection frameworks, as it is not part of the JSR-330 standard.

However, it is widely supported in Java EE and Jakarta EE environments.

In summary, @Resource is a name-first injection annotation that falls back to type-based resolution, while @Autowired and @Inject are primarily type-based.

If you prefer name-based injection or are working in a Java EE environment, @Resource is a suitable choice.

However, for Spring-specific features and flexibility, @Autowired is often preferred.

Both annotations can be used effectively in Spring applications, and Spring supports both.

The choice between @Resource @Autowired @Inject often depends on personal or team preference, as well as specific project requirements.

Overall, understanding the differences between these annotations helps in making informed decisions about dependency injection strategies in Spring applications.