IoC Container & Dependency Injection
Reviewed & published by Brayan K
The ApplicationContext is Spring's IoC container: it discovers your beans, builds the dependency graph, and injects collaborators for you — usually via constructor injection.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Before You Start
Make sure you've completed Setting Up a Spring Boot Project and understand plain dependency injection — Spring automates exactly that idea.
What You'll Learn
- What the ApplicationContext (IoC container) is
- What a bean is and how scanning finds it
- @Component and its stereotype variants
- Constructor injection and when @Autowired is optional
- How the container builds the dependency graph
- Why field injection is discouraged
The Big Idea — A Stage Manager Assembling the Cast
💡 Analogy: The IoC container is a stage manager. Each actor (bean) declares which props and co-stars they need; the stage manager fetches everyone, arranges them in the right order, and hands each actor exactly what they require before the show. The actors never go looking for their own props — control is inverted to the manager.
Your job is just to declare the cast (@Component) and their needs (constructor parameters); Spring directs the assembly.
1️⃣ Declaring Beans with @Component
Annotating a class with @Component (or a stereotype like @Service) registers it as a bean during component scanning. Spring then knows it can create and inject it.
// (Conceptual — needs Spring on the classpath to run.)
package com.example.demo;
import org.springframework.stereotype.Component;
// @Component marks this class so COMPONENT SCANNING registers it as a bean.
@Component
public class GreetingService {
public String greet(String name) {
return "Hello, " + name + "!";
}
}2️⃣ Constructor Injection
Declare the dependency as a final field and accept it in the constructor. Spring supplies the matching bean — and with a single constructor, @Autowired is optional.
package com.example.demo;
import org.springframework.stereotype.Service;
@Service // a specialised @Component (a service bean)
public class WelcomeService {
private final GreetingService greeting; // depend on the abstraction/bean
// CONSTRUCTOR INJECTION: Spring sees one constructor and supplies the
// matching GreetingService bean automatically. @Autowired is OPTIONAL here.
public WelcomeService(GreetingService greeting) {
this.greeting = greeting;
}
public String welcome(String name) {
return greeting.greet(name) + " Welcome aboard.";
}
}package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ReportRunner {
private final WelcomeService welcome;
// With a SINGLE constructor, this @Autowired is optional and could be
// omitted entirely — shown here only to make the injection point explicit.
@Autowired
public ReportRunner(WelcomeService welcome) {
this.welcome = welcome;
}
public String run(String name) {
return "[REPORT] " + welcome.welcome(name);
}
}3️⃣ How the Container Builds the Graph
At startup the container resolves dependencies and constructs beans in the correct order, injecting each one's collaborators. By default beans are singletons — one shared instance per container.
// HOW THE CONTAINER BUILDS THE GRAPH (mental model):
//
// 1. @SpringBootApplication triggers a component scan.
// 2. Spring finds GreetingService (@Component) and WelcomeService (@Service).
// 3. To build WelcomeService it needs a GreetingService -> it creates that
// bean FIRST, then injects it into WelcomeService's constructor.
// 4. Both beans now live in the ApplicationContext as SINGLETONS (one each).
//
// You never write 'new WelcomeService(new GreetingService())' — that wiring
// is Inversion of Control: the container, not your code, assembles objects.4️⃣ The Same Idea in Plain Java
To ground the concept, here is the identical wiring done by hand. Spring simply automates this — you are the container in plain Java.
import java.util.*;
import java.util.function.Supplier;
public class Main {
// 🎯 YOUR TURN — replace each ___ using the hint beside it.
//
// Spring's container is not magic. This is the whole idea in 30 lines:
// recipes in, wired objects out, one instance each.
interface Greeter { String greet(String name); }
static class PoliteGreeter implements Greeter {
public String greet(String name) { return "Good day, " + name + "."; }
}
static class Welcome {
// 1) Injected, and never reassigned afterwards.
private ___ Greeter greeter; // 👉 replace ___ with final
// Constructor injection: you cannot build one without its dependency.
Welcome(Greeter greeter) { this.greeter = greeter; }
String open(String name) { return greeter.greet(name) + " Welcome in."; }
}
static class Container {
private final Map<Class<?>, Supplier<?>> recipes = new LinkedHashMap<>();
private final Map<Class<?>, Object> singletons = new HashMap<>();
<T> void register(Class<T> type, Supplier<T> recipe) { recipes.put(type, recipe); }
@SuppressWarnings("unchecked")
<T> T get(Class<T> type) {
// 2) Already made? Hand back the same one. That is what
// "singleton scope" means, and it is the whole trick.
if (singletons.___(type)) return (T) singletons.get(type); // 👉 replace ___ with containsKey
Supplier<?> recipe = recipes.get(type);
// 3) Spring's NoSuchBeanDefinitionException is this check.
if (recipe == ___) throw new IllegalStateException("No bean of type " + type.getSimpleName()); // 👉 replace ___ with null
Object made = recipe.get();
singletons.put(type, made);
return (T) made;
}
}
public static void main(String[] args) {
Container ctx = new Container();
ctx.register(Greeter.class, PoliteGreeter::new);
// 4) Welcome needs a Greeter, so the recipe asks the container for one.
ctx.register(Welcome.class, () -> new Welcome(ctx.___(Greeter.class))); // 👉 replace ___ with get
Welcome w = ctx.get(Welcome.class);
System.out.println(w.open("Ada"));
// 5) Identity, not equality — is it literally the same object?
System.out.println("Same instance twice? " + (ctx.get(Welcome.class) ___ w)); // 👉 replace ___ with ==
try {
ctx.get(String.class);
} catch (IllegalStateException e) {
System.out.println("Asked for a bean it has not got: " + e.getMessage());
}
}
}🧠 Quick Recall
Predict before you peek.
Answer: ApplicationContext — it instantiates, wires and manages the lifecycle of beans.
Answer: when a bean has a single constructor — Spring uses it automatically (since 4.3).
Answer: singleton — exactly one shared instance per container.
Common Errors
- ❌ Forgetting @Component/@Service. The class isn't a bean and can't be injected. Fix: annotate it (or declare an @Bean).
- ❌ Field injection. Hides dependencies and breaks plain tests. Fix: use constructor injection.
- ❌ Two beans, one type. Causes NoUniqueBeanDefinitionException. Fix: add @Primary or @Qualifier.
- ❌ new-ing a managed bean yourself. Bypasses the container and its injection. Fix: let Spring create it.
- ❌ Bean outside the scanned packages. Never discovered. Fix: keep beans under the main class's package.
Pro Tips
- 💡 Always constructor-inject and mark fields final for explicit, immutable beans.
- 💡 Use the right stereotype — @Service for logic, @Repository for data, @Controller for web — for clearer intent.
- 💡 Program to interfaces so the container can inject any implementation.
- 💡 Drop @Autowired on single-constructor beans to reduce noise.
📋 Quick Reference
| Concept | Syntax / idea | Note |
|---|---|---|
| container | ApplicationContext | The IoC container |
| bean | @Component | Managed object |
| service bean | @Service | Stereotype of @Component |
| inject | constructor parameter | Preferred form |
| @Autowired | optional on 1 ctor | Inferred since 4.3 |
| default scope | singleton | One per container |
| ambiguity | @Primary / @Qualifier | Disambiguate |
📚 Resources / Keep Learning
- 📘 The IoC Container — Spring Framework docs
- 📗 Dependency Injection — Spring Framework docs
- 📗 Building an Application with Spring Boot — spring.io
🎉 Lesson Complete!
You now understand the ApplicationContext, what a bean is, how @Component and its stereotypes get discovered by scanning, how constructor injection works (and when @Autowired is optional), and how the container assembles the whole dependency graph.
Practice quiz
What is the Spring IoC container also known as?
- The compiler
- The ApplicationContext
- The classloader
- The garbage collector
Answer: The ApplicationContext. The ApplicationContext is Spring's IoC container: it instantiates, configures, wires and manages the lifecycle of beans.
What is a 'bean' in Spring?
- A primitive type
- An object instantiated, assembled and managed by the Spring container
- A SQL table
- A configuration file
Answer: An object instantiated, assembled and managed by the Spring container. A bean is simply an object whose creation and wiring is handled by the Spring IoC container.
What does @Component do?
- Marks a class so component scanning registers it as a bean
- Makes a class abstract
- Starts a new thread
- Deletes a bean
Answer: Marks a class so component scanning registers it as a bean. @Component flags a class for component scanning, so Spring detects it and registers it as a managed bean.
Which injection style does Spring recommend?
- Field injection
- Setter injection
- Static injection
- Constructor injection
Answer: Constructor injection. Constructor injection is recommended: dependencies are explicit, can be final, and the bean is always fully initialised.
On a class with a single constructor, @Autowired is...
- Required or it fails
- Optional — Spring infers it
- Forbidden
- Only for fields
Answer: Optional — Spring infers it. Since Spring 4.3, if a bean has exactly one constructor, @Autowired is optional; the container uses it automatically.
What does the container do with the dependency graph at startup?
- Ignores it
- Builds it: creates beans and injects collaborators in the right order
- Stores it in a database
- Prints it only
Answer: Builds it: creates beans and injects collaborators in the right order. Spring resolves dependencies and constructs the object graph, injecting each bean's collaborators as it builds them.
By default, how many instances of a singleton-scoped bean exist per container?
- One per request
- One per thread
- Exactly one, shared
- One per method call
Answer: Exactly one, shared. The default 'singleton' scope means a single shared instance is created per Spring container.
Why is field injection discouraged?
- It is faster but unsafe
- It hides dependencies and makes plain-Java testing harder
- It cannot compile
- It only works in Kotlin
Answer: It hides dependencies and makes plain-Java testing harder. Field injection hides what a class needs and prevents final fields, complicating testing without the container.
Which annotations are specialised forms of @Component?
- @Override and @Deprecated
- @Service, @Repository and @Controller
- @Test and @BeforeEach
- @Entity and @Table
Answer: @Service, @Repository and @Controller. @Service, @Repository and @Controller are stereotype annotations meta-annotated with @Component for clearer intent.
What happens if Spring finds two beans matching a single dependency type?
- It picks at random silently
- It throws an error unless you disambiguate (e.g. @Qualifier/@Primary)
- It merges them
- It ignores both
Answer: It throws an error unless you disambiguate (e.g. @Qualifier/@Primary). An ambiguous dependency causes a NoUniqueBeanDefinitionException unless you mark a @Primary or use @Qualifier.
Continue this course
- Previous: Setting Up a Spring Boot Project
- Next: Beans, Scopes & Configuration — @Bean, @Configuration, singleton vs prototype scopes, @Qualifier and @Primary
- Quick reference: Java cheat sheet
Frequently asked questions
What is the Spring IoC container?
The IoC (Inversion of Control) container is the heart of Spring, represented by the ApplicationContext interface. Its job is to instantiate the objects your application needs (called beans), configure them, wire their dependencies together, and manage their lifecycle. 'Inversion of Control' means that instead of your classes creating their own collaborators with new, the container takes over that responsibility and supplies the collaborators from outside. You describe what beans exist and what they depend on — through annotations like @Component and constructor parameters — and the container figures out the construction order and assembles the whole object graph for you at startup.
How does Spring know which classes to manage as beans?
Through component scanning. The @SpringBootApplication annotation enables @ComponentScan, which inspects the main class's package and all sub-packages for classes annotated with @Component or one of its specialisations — @Service, @Repository and @Controller. Each detected class is registered as a bean definition in the ApplicationContext. This is why the main class should sit at the root of your package tree: anything below it is scanned automatically. You can also declare beans explicitly with @Bean methods in a @Configuration class, which is useful for third-party types you can't annotate.
Why is constructor injection preferred over field injection in Spring?
Constructor injection makes dependencies explicit (you can read everything a class needs from its constructor signature), allows the fields to be final (so they're set exactly once and can never be null), and guarantees the bean is in a fully valid state the moment it exists. It also keeps the class testable in plain Java: you can call new with fakes, no Spring container required. Field injection — annotating a private field with @Autowired — hides dependencies, prevents final fields, and forces tests to use reflection or the container. Since Spring 4.3, a bean with a single constructor doesn't even need @Autowired, so constructor injection is also the most concise option.
What does @Autowired actually do?
@Autowired tells Spring to resolve and inject a matching bean from the container at the point it is placed — a constructor, a setter, or (less ideally) a field. Spring looks up a bean whose type satisfies the dependency and supplies it. On a constructor it expresses 'build this bean by handing me these collaborators'. Importantly, if a class has exactly one constructor, @Autowired is optional there because Spring will use that constructor automatically. If more than one bean matches a required type, Spring cannot choose and throws a NoUniqueBeanDefinitionException — you then disambiguate with @Primary or @Qualifier.
Is Spring's dependency injection different from plain-Java DI?
Conceptually it is identical — both supply a class's dependencies from outside rather than having the class create them. The difference is who does the wiring. In plain Java, your main method or a factory creates each object and passes collaborators into constructors by hand. In Spring, you annotate classes as beans and the container performs that same wiring automatically, resolving the entire dependency graph and managing object lifecycles and scopes. So learning plain-Java constructor injection first is the perfect foundation: Spring is just an industrial-strength automation of the very same principle.