Tiny Guide to Spring IoC and ApplicationContext πŸ“¦πŸŒ±

Why IoC

Normal Java does:


UserService userService = new UserService();
OrderService orderService = new OrderService(userService);
  

So user has to:

  • Manually create each object
  • Manually connect dependencies
  • Handle object lifecycle on their own
  • Deal with difficult testing and maintenance

It’s a lot to maintain 🀯❌

Therefore SpringBoot uses IoC


What is IoC

IoC (Inversion of Control) is one of the core ideas behind the Spring Framework. With IoC:

  • Spring creates the objects (beans)
  • Spring injects the dependencies
  • Spring manages the lifecycle
  • User simply declare what you need

The name of Inversion of Control means SpringBoot manage all of these ranther than user does


ApplicationContext

ApplicationContext is the implementation of IoC container

🌳 Full Lifecycle
πŸš€ 1. Bootstrapping
SpringApplication.run(), prepare environment, load properties, process profiles
πŸ“¦ 2. Create ApplicationContext
Instantiate context (AnnotationConfigApplicationContext / WebApplicationContext)
πŸ“„ 3. Load Bean Definitions
Scan @Configuration, @ComponentScan, XML, @Bean methods β†’ register BeanDefinition
🏭 4. Prepare BeanFactory
Register BeanPostProcessors, MessageSource, ConversionService, ApplicationEventMulticaster
βš™οΈ 5. Spring Boot Auto-Configuration
Apply auto-config classes (web server, security, data, Jackson, MVC, Actuator…)
πŸ•ΈοΈ 6. Start Embedded Web Server (if web)
Setup Tomcat/Jetty/Netty, register DispatcherServlet, init web infrastructure
🫘 7. Create Beans (BeanFactory)
Instantiate β†’ inject dependencies β†’ apply BeanPostProcessors β†’ @PostConstruct / InitializingBean
πŸ“’ 8. Publish ContextRefreshedEvent
All non-lazy singleton beans have been fully created and initialized.
Spring publishes ContextRefreshedEvent to signal that the BeanFactory’s creation phase is complete.
πŸƒβ€β™€οΈ 9. Run CommandLineRunner / ApplicationRunner
Execute startup logic after context is ready, before the app is marked as fully started
βœ… 10. ApplicationReadyEvent
Published when the application is fully started and server is ready; ideal for warm-up tasks
🟒 11. Running
Serve web requests, dispatch events, manage Beans, AOP, transactions
🧹 12. Shutdown
Publish ContextClosedEvent β†’ call @PreDestroy / DisposableBean.destroy() β†’ release resources

ApplicationContext creates and manages a simple object called bean 🫘. When user annotates a class with @Service, @Component, @Controller or define a @Bean method, Spring turns those into beans.


Workflow of Bean Creation while SpringBoot starts

1. When the application launches, Spring Boot does:


SpringApplication.run("MyApp.class");

This triggers Spring to create an ApplicationContext instance, which will hold all beans

2. Spring scans your packages

Spring looks for classes annotated with:

  • @Component
  • @Service
  • @Repository
  • @Controller / @RestController
  • @Configuration
  • and any class discovered under component scan

This is called classpath scanning.

3. Spring registers BeanDefinitions

When Spring scans your application for components, it does not create objects immediately.
Instead, Spring creates metadata called a BeanDefinition.

4. Spring creates bean instances

After analyzing all definitions, Spring starts creating beans:

  • It looks at constructor parameters
  • Resolves dependencies
  • Creates the object
  • Stores it in the ApplicationContext

5. Spring injects dependencies

Next, Spring performs dependency wiring:

  • If Bean A needs Bean B β†’ B is injected
  • If the bean uses fields with @Autowired β†’ reflection injects
  • If the bean uses constructor injection β†’ resolved in the step 4
  • If the bean uses setter injection β†’ setter is called

Dependencies are now fully wired.

6. Spring calls lifecycle callbacks

If a bean implements:

  • InitializingBean
  • DisposableBean
  • @PostConstruct
  • @PreDestroy

Spring calls these hooks at the appropriate time.

7. The ApplicationContext is fully initialized

At this point:

  • All beans are created
  • All dependencies are resolved
  • All configurations are applied
  • Filters, providers, and security components are registered
  • The application is ready to serve requests

Best Practise for CommandLineRunner/ ApplicationRunner

1. Use it to run one-off tasks

βœ… Typical use cases include:

  • Seeding initial data (e.g. default admin user, basic configuration)
  • Verifying that external dependencies are reachable (e.g. third-party APIs, storage services)
  • Performing lightweight warm-up tasks (e.g. pre-loading caches)

❌ They should not contain:

  • Infinite loops or long-running background tasks, such as while(True)
  • Core business workflows that are expected to run repeatedly

@Component
public class AdminUserInitializer implements CommandLineRunner {

    private final UserService userService;

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

    @Override
    public void run(String... args) {
        // create admin account if it does not exist
        userService.createAdminUserIfMissing();
    }
}

A runner should mainly act as aΒ trigger, not as a place to implement business logic.

2. Control where it runs (profiles / environments)

Spring Boot automatically loads configuration files based on theΒ active profile.

For example, if Spring Boot has:

src/main/resources/
│── application.yml
│── application-dev.yml
│── application-prod.yml
When set pring.profiles.active=dev
Spring Boot loads files in this order:
application.yml
application-dev.yml β†’ overrides same properties

And CommandLineRunner files know when they should execute by @Profile


@Component
@Profile({"dev", "local"})
public class DevAdminInitializer implements CommandLineRunner {
    // ...
}

We can tell Spring Boot which environment we are in different ways:

Option A: In application.yml
spring:
  profiles:
    active: dev

Option B: JVM parameter (recommended for prod deploy)
java -jar app.jar –spring.profiles.active=prod

Option C: Environment variable
SPRING_PROFILES_ACTIVE=prod

3. Avoid hard-coding secrets

We can use @value annotation in the CommandLineRunner file


@Component
public class AdminInitializer implements CommandLineRunner {

    @Value("${app.admin.username}")
    private String username;

    @Value("${app.admin.password}")
    private String rawPassword;

    @Override
    public void run(String... args) {
        userService.createAdminIfMissing(username, rawPassword);
    }
}

and application.yml


app:
  admin:
    username: admin
    password: ${APP_ADMIN_PASSWORD:changeme}

Leave a Reply

Discover more from Daily Learning

Subscribe now to keep reading and get access to the full archive.

Continue reading