The IoC Container & Beans
Inversion of Control is a simple rename of a powerful idea: instead of your objects creating their collaborators with new, a container creates them and hands them over.
Introduction
Inversion of Control is a simple rename of a powerful idea: instead of your objects creating their collaborators with new, a container creates them and hands them over. Your code becomes a recipe ("I need a PaymentGateway"); the container is the kitchen that fetches the ingredient.
In Spring, the container is called the ApplicationContext, and every object it manages is called a bean. The container knows how to build each bean, when to build it, how long to keep it, and how to wire it to other beans. Everything else in Spring — MVC, transactions, security — is just beans plugged into this same container.
Understanding the topic
Bean lifecycle in plain English:
- Spring scans configuration → discovers bean definitions.
- It resolves the dependency graph and orders the beans for creation.
- It creates each bean using a constructor (or factory method).
- It injects the bean's dependencies.
- It applies any BeanPostProcessors (this is where AOP proxies get added).
- It calls any
@PostConstructinitialisation hook. - The bean serves requests for as long as the context lives.
- On shutdown,
@PreDestroyruns and the bean is released.
ApplicationContext vs BeanFactory: BeanFactory is the bare minimum container; ApplicationContext extends it with event publication, message resolution, environment access and AOP integration. In application code you always use the latter.
Syntax reference
Declaring beans with Java configuration:
@Configurationpublic class AppConfig {@Beanpublic PaymentGateway paymentGateway() {return new StripePaymentGateway("sk_test_...");}@Beanpublic CheckoutService checkoutService(PaymentGateway gateway) {return new CheckoutService(gateway); // Spring passes the bean above}}
Informative example
Starting the container, using a bean, and observing lifecycle hooks:
public class Demo {public static void main(String[] args) {var ctx = new AnnotationConfigApplicationContext(AppConfig.class);CheckoutService checkout = ctx.getBean(CheckoutService.class);checkout.placeOrder(cart);ctx.close(); // @PreDestroy hooks run here}}@Componentclass AuditService {@PostConstruct void init() { System.out.println("ready"); }@PreDestroy void cleanup(){ System.out.println("bye"); }}
Run this and you will see ready printed during startup and bye printed at shutdown — proof that the container, not your code, is driving the lifecycle.
Real-world use
Production apps register hundreds of beans: data sources, HTTP clients, schedulers, caches, message listeners. The container resolves their dependency order automatically, which is why you almost never have to think about startup sequencing in a Spring app.
Best practices
- Keep beans stateless wherever possible — they are shared by default.
- Use
@Configurationclasses; reach for XML only when maintaining legacy code. - Group related beans into focused
@Configurationclasses (e.g.SecurityConfig,PersistenceConfig).
Common mistakes
- Forgetting that singleton beans are shared across threads — never hold per-request state in fields.
- Calling
newon a service inside a bean — you bypass the container and lose DI, AOP, and lifecycle hooks. - Doing heavy work in a constructor — it blocks application startup; use
@PostConstructif you must.
Hands-on exercise
Try this: add a @PostConstruct method to two beans where one depends on the other. Print a message from each. Confirm that the dependency is created first. Then add a third bean with a @PreDestroy method and verify the shutdown order is the reverse of creation.