Stage 2 — Spring Core: Bean First, Then IoC and DI
What a Bean is in Spring
A Bean is an object created, registered, and managed by ApplicationContext.
In plain Java, you instantiate with new and own lifecycle management yourself.
class PaymentClient {
void charge(String orderId) {
System.out.println("Charging order " + orderId);
}
}
class OrderService {
private final PaymentClient paymentClient;
OrderService() {
this.paymentClient = new PaymentClient();
}
}
This code is simple, but OrderService now decides how PaymentClient is created. If you later need a test version, a sandbox version, logging, metrics, or transaction behavior around that dependency, you must change object creation code yourself. Spring changes the ownership model: you describe which classes should become Beans and which dependencies they need, then the container creates the runtime object graph.
In Spring, once a class becomes a Bean, it is part of the container-managed application. Its behavior depends not only on class code, but also on configuration, active profiles, post-processors, lifecycle callbacks, and sometimes proxies. That is why “Bean” does not mean “any Java object”. It means an object known to the container.
| Concept | Plain Java object | Spring Bean |
|---|---|---|
| Creation | Usually new in your code | Created from bean definitions |
| Dependencies | Passed manually by developer code | Injected by the container |
| Lifecycle | Owned by caller | Managed by ApplicationContext |
| Infrastructure | No automatic Spring behavior | Can receive proxies, callbacks, profiles |
Where Beans come from
Container gets bean definitions mainly from component scanning (@Component, @Service, @Repository, @Controller) and Java configuration (@Configuration + @Bean). Scanning is convenient for application code because Spring finds annotated classes under configured packages. @Bean is useful for third-party classes or when you need precise creation rules.
Critical distinction: if you create something manually with new in a random method, the container does not automatically manage that object. For example, @Transactional, @Async, validation proxies, and lifecycle callbacks apply to Spring-managed Beans, not to arbitrary objects you created yourself.
IoC (Inversion of Control)
IoC means Inversion of Control. The phrase sounds abstract, but the idea is simple: the place that controls object creation and wiring changes. In manual Java code, your classes usually decide what collaborators to instantiate and how to connect them. In Spring, the container owns this composition step. Your code declares needs, and the container satisfies those needs.
Without IoC, OrderService might create PaymentClient, InventoryClient, and OrderRepository inside its constructor. That makes the service responsible for business behavior and object graph construction at the same time. With IoC, OrderService says: “I need these dependencies.” Spring decides which concrete Beans match those dependencies, creates them, injects them, and keeps them available during application runtime.
This is useful because large applications change constantly. Today you use a real payment client; tomorrow you need sandbox payment in dev, a mock in tests, and a metrics wrapper in production. If every class creates its own dependencies, changes spread across many files. If the container owns composition, many changes become configuration-level decisions.
DI (Dependency Injection)
DI means Dependency Injection. It is the main technique Spring uses to implement IoC. A dependency is another object a class needs to do its work. Injection means that the dependency is provided from outside instead of being created inside the class.
Constructor injection is the default style for important application code:
@Service
class OrderService {
private final PaymentClient paymentClient;
private final OrderRepository orderRepository;
OrderService(PaymentClient paymentClient, OrderRepository orderRepository) {
this.paymentClient = paymentClient;
this.orderRepository = orderRepository;
}
}
Here OrderService is honest about its requirements. It cannot exist without PaymentClient and OrderRepository, so those dependencies are constructor arguments. Spring sees the constructor, finds matching Beans, and passes them in. The class is easier to test because a unit test can pass fake dependencies directly. The class is also easier to understand because its contract is visible at the top.
Field injection often feels faster, but hides the class contract and makes tests harder. If a dependency is required for valid object state, constructor injection expresses that requirement clearly.
Bean lifecycle and post-processing
Bean lifecycle is more than “create and use”. Spring reads bean definitions, instantiates objects, injects dependencies, calls initialization callbacks, applies post-processors, makes the Bean available, and later calls destroy callbacks when the context closes.
Post-processing is especially important. Spring can wrap a Bean with a proxy for transactions, security, caching, observability, or AOP. That is why runtime behavior may differ from plain class implementation. When you call a method on a proxied Bean, you may actually pass through infrastructure code before the target method runs.
This explains many confusing debugging cases. If @Transactional does not work, the issue is often not the SQL code, but that the object was created manually, self-invoked inside the same class, or not called through the Spring proxy.
Scope: why singleton is not global
In Spring, singleton means “one instance per container”, not one per JVM globally. Multiple application contexts imply multiple singleton instances. This matters in tests, modular applications, and applications that start child contexts. The scope describes how the container reuses Bean instances, not a universal Java rule.
ApplicationContext vs BeanFactory
BeanFactory is the minimal DI container abstraction. ApplicationContext is the full runtime container with events, profiles, i18n, resource loading, environment access, and post-processing infrastructure. In Spring Boot applications, you effectively work with ApplicationContext, even if most code never touches it directly.
What happens during startup
At startup Spring reads configuration and classpath, builds bean definitions, resolves dependencies, creates singleton Beans, applies post-processors, runs initialization callbacks, and publishes a ready context. The important part is causal reasoning: startup failures usually map to one of these steps. A missing Bean means dependency resolution failed. A circular dependency means the graph cannot be built cleanly. A failed init method means the object was created, but readiness failed.
Practical example
Imagine an order-processing service. With manual wiring, you create OrderService, then explicitly create PaymentClient, InventoryClient, OrderRepository, and pass dependencies by hand. In Spring, the same composition is modeled as Beans: container reads definitions, builds the object graph, injects dependencies, and manages lifecycle consistently. The difference becomes obvious when switching PaymentClient to sandbox mode for dev: manual wiring needs multiple code edits, container model usually needs config-level bean selection.
The graph matters because OrderService should coordinate business steps, not decide the concrete construction strategy for every collaborator. If inventory moves to another API, the service constructor contract may stay the same while only the InventoryClient implementation changes. If persistence changes, OrderRepository can be swapped behind the same dependency boundary. IoC and DI make those replacement points explicit.
Stage takeaway
When Bean mechanics are clear, IoC and DI stop being interview vocabulary and become an operational model. Bean means “container-managed object”. IoC means “container controls composition”. DI means “dependencies are provided from outside”. Together, they let Spring build and maintain a consistent runtime graph instead of forcing every class to assemble the application manually.
Nuances and common mistakes
- Creating objects with
newinside services and expecting@Transactionalor@Asyncbehavior. - Overusing field injection and hiding dependency contracts.
- Keeping mutable shared state inside singleton Beans without concurrency controls.
- Misplacing package structure so component scanning misses part of the app.
- Treating
ApplicationContextas a service locator everywhere instead of declaring dependencies in constructors.
Understanding checklist
- I can explain how Bean differs from a plain Java object.
- I can describe Bean lifecycle from definition to destroy.
- I understand why constructor injection is the default best practice.
- I understand why IoC moves object graph construction out of business classes.
- I understand why singleton scope is container-bound, not JVM-global.