Spring BootInversion of Control (IoC)
Spring Boot

Inversion of Control (IoC)

Inversion of Control is the foundational design principle behind Spring Framework. Instead of objects creating and managing their own dependencies, control is inverted — the Spring IoC container takes responsibility for creating objects, wiring dependencies, and managing their lifecycle. Understanding IoC is understanding why Spring works the way it does.

What Is Inversion of Control?

Inversion of Control (IoC) is a design principle where the control of object creation and dependency management is transferred from the application code to an external container or framework. The word "inversion" refers to reversing the traditional flow: instead of your code creating the objects it needs, a framework creates and provides them for you. In traditional Java code, a class that needs a collaborator creates it directly: UserService creates a new UserRepository. UserRepository creates a new DatabaseConnection. DatabaseConnection creates a new ConnectionPool. Each object owns and manages everything it depends on. This creates tight coupling — changing any piece forces changes throughout the chain. With IoC, none of this happens in your code. You declare what you need, and the IoC container figures out how to provide it. The container creates objects, resolves dependencies, and assembles the application. Your classes are completely decoupled from the creation and lifecycle of their dependencies. Spring's IoC container is the core of the entire Spring ecosystem. Every feature — dependency injection, AOP, transactions, security, data access — is built on top of the container.

The Problem IoC Solves

To understand why IoC matters, look at what code looks like without it — and the real problems that creates.
Java
// WITHOUT IoC — traditional object creation (tight coupling):
public class OrderService {

    // OrderService creates its own dependencies — tightly coupled:
    private final OrderRepository orderRepository = new OrderRepositoryImpl();
    private final PaymentService paymentService = new StripePaymentService(
        new StripeClient("sk_live_abc123"),    // hardcoded API key!
        new RetryPolicy(3, 1000)
    );
    private final EmailService emailService = new SmtpEmailService(
        "smtp.gmail.com", 587, "user@gmail.com", "password123"  // hardcoded credentials!
    );
    private final InventoryService inventoryService = new InventoryServiceImpl(
        new InventoryRepository(new DatabaseConnection("jdbc:mysql://..."))
    );

    public Order createOrder(OrderRequest request) {
        // business logic...
    }
}

// Problems with this approach:
// 1. IMPOSSIBLE TO TEST — can't replace StripePaymentService with a mock
//    without modifying OrderService source code
// 2. HARDCODED CONFIGURATION — API keys, DB URLs, credentials in code
// 3. DUPLICATED OBJECT CREATION — every class that needs EmailService
//    creates its own instance — no sharing, no singleton behavior
// 4. RIGID COUPLING — want to switch from Stripe to PayPal?
//    Find every new StripePaymentService() across the codebase and change them
// 5. IMPOSSIBLE TO REUSE — OrderService is inseparable from its dependencies

// WITH Spring IoC — declare what you need, Spring provides it:
@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
    private final EmailService emailService;
    private final InventoryService inventoryService;

    // Declare dependencies in constructor — Spring resolves and injects them:
    public OrderService(OrderRepository orderRepository,
                        PaymentService paymentService,
                        EmailService emailService,
                        InventoryService inventoryService) {
        this.orderRepository = orderRepository;
        this.paymentService = paymentService;
        this.emailService = emailService;
        this.inventoryService = inventoryService;
    }

    public Order createOrder(OrderRequest request) {
        // same business logic — but now testable, configurable, flexible
    }
}
// OrderService has NO IDEA how PaymentService is created or configured.
// In production: real Stripe implementation injected.
// In tests: mock injected — zero code changes to OrderService.

The Spring IoC Container

Spring's IoC container is represented by the ApplicationContext interface. It reads configuration (annotations, Java config, or XML), creates all the beans, resolves their dependencies, and manages their lifecycle from startup to shutdown.
Java
// ApplicationContext is the Spring IoC container:
// It's created automatically by SpringApplication.run() in Spring Boot

// The container hierarchy:
// BeanFactory (basic container — lazy, minimal features)
//   └── ApplicationContext (full container — eager, all features)
//         ├── AnnotationConfigApplicationContext     (Java/annotation config)
//         ├── ClassPathXmlApplicationContext         (XML config — legacy)
//         └── AnnotationConfigServletWebServerApplicationContext  (Spring Boot web)

// Spring Boot creates the right ApplicationContext automatically:
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        // This creates and starts the ApplicationContext:
        ConfigurableApplicationContext context =
            SpringApplication.run(Application.class, args);

        // You can retrieve beans from the context manually (rarely needed):
        UserService userService = context.getBean(UserService.class);
        OrderService orderService = context.getBean("orderService", OrderService.class);

        // List all bean names:
        String[] beanNames = context.getBeanDefinitionNames();

        // Check if a bean exists:
        boolean exists = context.containsBean("userService");

        // The context is the IoC container — it holds ALL your beans.
    }
}

// What the IoC container does:
// 1. READS configuration (your @Component, @Service, @Bean annotations)
// 2. CREATES BeanDefinitions — metadata describing how to create each bean
// 3. RESOLVES dependencies — determines what each bean needs
// 4. INSTANTIATES beans — creates objects in the right order
// 5. INJECTS dependencies — wires beans together
// 6. CALLS lifecycle callbacks — @PostConstruct, InitializingBean, etc.
// 7. MAKES beans available — for injection anywhere in the application
// 8. DESTROYS beans on shutdown — @PreDestroy, DisposableBean, etc.

How Spring Discovers Beans

The IoC container needs to know which classes to manage. Spring discovers beans through component scanning, Java configuration, and explicit bean registration.
Java
// METHOD 1 — Component Scanning (most common):
// @SpringBootApplication triggers @ComponentScan on the root package
// Any class annotated with a stereotype is discovered and registered:

@Component       // generic component
@Service         // business logic layer
@Repository      // data access layer
@Controller      // web controller
@RestController  // REST API controller
@Configuration   // configuration class

// Spring scans com.example.myapp and ALL sub-packages automatically
// No manual registration — just annotate and Spring finds it

// METHOD 2@Bean methods in @Configuration classes:
@Configuration
public class InfrastructureConfig {

    // Spring calls this method and registers the return value as a bean:
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }

    // Dependencies of @Bean methods are injected automatically:
    @Bean
    public JwtTokenService jwtTokenService(JwtProperties properties) {
        return new JwtTokenService(properties.getSecret(), properties.getExpiration());
    }

    // @Bean with dependencies from the container:
    @Bean
    public DataSource dataSource(DataSourceProperties properties) {
        return DataSourceBuilder.create()
            .url(properties.getUrl())
            .username(properties.getUsername())
            .password(properties.getPassword())
            .build();
    }
}

// METHOD 3 — Programmatic registration (advanced):
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(Application.class);
        app.addInitializers((ApplicationContextInitializer<GenericApplicationContext>) ctx -> {
            ctx.registerBean(MySpecialService.class, () -> new MySpecialService("custom"));
        });
        app.run(args);
    }
}

IoC vs Traditional Control Flow

The difference between IoC and traditional programming isn't just a code pattern — it's a fundamental shift in which code is in control of the application's composition.
Java
// TRADITIONAL CONTROL (your code is in charge):
// main() creates everything manually
public class TraditionalApp {
    public static void main(String[] args) {
        // Your code assembles the entire application:
        DatabaseConnection db = new DatabaseConnection("jdbc:mysql://localhost/mydb");
        UserRepository userRepo = new UserRepositoryImpl(db);
        EmailClient emailClient = new SmtpEmailClient("smtp.gmail.com");
        EmailService emailService = new EmailServiceImpl(emailClient);
        PasswordEncoder encoder = new BCryptPasswordEncoder();
        UserService userService = new UserServiceImpl(userRepo, emailService, encoder);
        UserController controller = new UserController(userService);

        // Start a web server and route requests to controller...
        // YOU control everything — YOUR code creates, wires, manages
    }
}

// IoC (the container is in charge):
// Your code declares intentions — Spring does the assembly
@SpringBootApplication
public class SpringApp {
    public static void main(String[] args) {
        SpringApplication.run(SpringApp.class, args);
        // THE CONTAINER controls everything after this line:
        // - Scans for @Component, @Service, @Repository, @Controller
        // - Creates UserRepositoryImpl, EmailServiceImpl, UserServiceImpl, UserController
        // - Resolves: UserController needs UserService, UserService needs UserRepository...
        // - Creates beans in dependency order
        // - Injects dependencies through constructors
        // - Starts embedded Tomcat
        // - Routes HTTP requests to controllers
        // YOU wrote the classes. SPRING assembled and wired them.
    }
}

// The Hollywood Principle — "Don't call us, we'll call you":
// Your classes don't call Spring to get their dependencies.
// Spring calls your constructors and passes the dependencies in.
// Control is inverted — Spring drives, your code responds.

ApplicationContext Types

Spring provides different ApplicationContext implementations for different scenarios. Spring Boot selects the right one automatically based on what's on the classpath.
Java
// Spring Boot auto-selects the ApplicationContext type:

// 1. AnnotationConfigServletWebServerApplicationContext
//    → when spring-boot-starter-web is present (Tomcat/Jetty)
//    → supports @Controller, @RestController, embedded server

// 2. AnnotationConfigReactiveWebServerApplicationContext
//    → when spring-boot-starter-webflux is present (Netty)
//    → supports reactive @RestController, WebFlux

// 3. AnnotationConfigApplicationContext
//    → when no web starter is present (batch, CLI tools)
//    → no embedded server

// You can force the type:
SpringApplication app = new SpringApplication(Application.class);
app.setWebApplicationType(WebApplicationType.NONE);     // no server
app.setWebApplicationType(WebApplicationType.REACTIVE); // Netty
app.run(args);

// Accessing the ApplicationContext in a bean:
@Component
public class BeanInspector implements ApplicationContextAware {

    private ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext ctx) {
        this.context = ctx;
    }

    public void printAllBeans() {
        Arrays.stream(context.getBeanDefinitionNames())
              .sorted()
              .forEach(System.out::println);
    }
}

// Or inject it directly:
@Service
public class DynamicBeanLoader {

    @Autowired
    private ApplicationContext context;

    public <T> T getBean(Class<T> type) {
        return context.getBean(type);
    }
}

// ApplicationContext also provides:
context.getEnvironment()           // access properties and profiles
context.getResource("file.txt")    // load classpath/file resources
context.publishEvent(new MyEvent(this))  // publish application events
context.getMessage("key", null, Locale.US) // i18n message resolution

IoC Benefits in Practice

IoC's benefits become concrete when you look at what it enables across testing, configuration, and architecture.
Java
// BENEFIT 1 — Effortless unit testing:
// Without IoC: impossible to test OrderService without real Stripe/DB
// With IoC: inject mocks through the constructor

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock OrderRepository orderRepository;
    @Mock PaymentService paymentService;    // mock — no real Stripe calls
    @Mock EmailService emailService;        // mock — no real emails sent
    @InjectMocks OrderService orderService;

    @Test
    void createOrder_chargesPaymentAndSendsEmail() {
        // Arrange
        OrderRequest request = new OrderRequest("PROD-1", 2, "user@test.com");
        when(paymentService.charge(any())).thenReturn(new PaymentResult("txn_123"));

        // Act
        Order order = orderService.createOrder(request);

        // Assert
        verify(paymentService).charge(any());
        verify(emailService).sendConfirmation(eq("user@test.com"), any());
        verify(orderRepository).save(any());
    }
}
// OrderService code: ZERO changes for testing

// BENEFIT 2 — Environment-specific implementations via profiles:
@Service
@Profile("dev")
public class MockPaymentService implements PaymentService {
    public PaymentResult charge(PaymentRequest request) {
        return new PaymentResult("mock-txn-" + UUID.randomUUID());
    }
}

@Service
@Profile("prod")
public class StripePaymentService implements PaymentService {
    public PaymentResult charge(PaymentRequest request) {
        return stripe.charges().create(request);
    }
}
// OrderService: completely unaware which implementation it's using
// Swap implementations by changing a profile — zero code changes

// BENEFIT 3 — Singleton management:
// The IoC container creates ONE instance of each singleton bean
// and shares it across the entire application
// No static variables, no manual singleton patterns
@Service
public class CacheService { }  // one instance, shared by all who inject it

// BENEFIT 4 — Lifecycle management:
@Service
public class ReportScheduler {

    @PostConstruct
    public void start() {
        System.out.println("ReportScheduler started");
        // Initialize resources after all dependencies are injected
    }

    @PreDestroy
    public void stop() {
        System.out.println("ReportScheduler stopped");
        // Clean up on graceful shutdown
    }
}