The IoC container and dependency injection
What the ApplicationContext actually does at startup — read bean definitions, build each singleton after its dependencies, and inject the same instance everywhere.
After this lesson you can
- Explain inversion of control in terms of who calls the constructor
- Predict the order in which the container creates singleton beans
- Say why constructor injection is preferred and what a constructor cycle does at startup
Without a container, an OrderService builds its own collaborators:
new JdbcOrderRepository(new HikariDataSource(...)). The class decides
which implementation it gets and how it is wired. Inversion of
control turns that around: the class only declares what it needs,
usually as constructor parameters, and something else — the Spring
ApplicationContext — decides what to pass in. Handing a dependency in
from outside is dependency injection; IoC is the broader idea that
the framework, not your code, drives object creation.
@Service
public class OrderService {
private final OrderRepository orders;
private final Clock clock;
// A single constructor needs no @Autowired: Spring uses it.
public OrderService(OrderRepository orders, Clock clock) {
this.orders = orders;
this.clock = clock;
}
}
@Configuration
class TimeConfig {
@Bean
Clock clock() { return Clock.systemUTC(); }
}
A bean is an object whose lifecycle the container manages. Spring
finds bean definitions in two ways: component scanning picks up classes
annotated @Component (or @Service, @Repository, @Controller,
which are specialisations of it) in the package of the
@SpringBootApplication class and below; and @Bean methods in
@Configuration classes register whatever they return.
What happens at startup
When the context refreshes, it pre-instantiates every singleton bean
(unless the bean is marked @Lazy). For each one it resolves the
constructor arguments first, which means asking the container for those
beans — creating them on the spot if they do not exist yet. Each
singleton is created once, cached, and the cached instance is what every
other bean receives. The model below is that algorithm in a few lines:
definitions are visited in registration order, but dependencies are
always built before the bean that needs them.
import java.util.*;import java.util.function.Function; public class Solution { static final class Container { private final Map<String, List<String>> deps = new LinkedHashMap<>(); private final Map<String, Function<List<Object>, Object>> factories = new HashMap<>(); private final Map<String, Object> singletons = new HashMap<>(); final List<String> log = new ArrayList<>(); void register(String name, List<String> dependsOn, Function<List<Object>, Object> factory) { deps.put(name, dependsOn); factories.put(name, factory); } Object getBean(String name) { Object cached = singletons.get(name); if (cached != null) return cached; List<Object> args = new ArrayList<>(); for (String d : deps.get(name)) args.add(getBean(d)); // dependencies first Object bean = factories.get(name).apply(args); log.add("created " + name); singletons.put(name, bean); return bean; } void refresh() { for (String name : deps.keySet()) getBean(name); // pre-instantiate singletons } } static final class Clock {} static final class OrderRepository { final Clock clock; OrderRepository(Clock c) { clock = c; } } static final class AuditLog { final Clock clock; AuditLog(Clock c) { clock = c; } } static final class OrderService { final OrderRepository repo; final AuditLog audit; OrderService(OrderRepository r, AuditLog a) { repo = r; audit = a; } } public static List<String> run() { Container ctx = new Container(); // Registered in an order that is NOT the order they can be built in. ctx.register("orderService", List.of("orderRepository", "auditLog"), a -> new OrderService((OrderRepository) a.get(0), (AuditLog) a.get(1))); ctx.register("orderRepository", List.of("clock"), a -> new OrderRepository((Clock) a.get(0))); ctx.register("auditLog", List.of("clock"), a -> new AuditLog((Clock) a.get(0))); ctx.register("clock", List.of(), a -> new Clock()); ctx.refresh(); OrderService svc = (OrderService) ctx.getBean("orderService"); List<String> out = new ArrayList<>(ctx.log); out.add("same clock in both: " + (svc.repo.clock == svc.audit.clock)); return out; }}Why constructor injection
Field injection (@Autowired private OrderRepository orders;) works,
but the object can exist with the field still null, the field cannot
be final, and a unit test has to use reflection or a Spring context to
set it. Constructor injection makes the dependencies explicit, lets
them be final, and lets a test call new OrderService(fakeRepo, fixedClock) with no Spring at all.
It also surfaces design problems early. If A needs B in its
constructor and B needs A, neither can be constructed first. The
container detects the loop when it is asked for a bean that is already
being created and fails the startup with a
BeanCurrentlyInCreationException. Since Spring Boot 2.6, circular
references are refused by default even with field injection
(spring.main.allow-circular-references is false), so the honest fix
is to break the cycle — usually by extracting the shared logic into a
third bean.
When more than one bean fits
Injection is by type. If two beans implement PaymentGateway, asking
for a PaymentGateway is ambiguous and startup fails with
NoUniqueBeanDefinitionException. Mark one bean @Primary to make it
the default, or put @Qualifier("stripe") on the injection point to
name the one you want. Injecting receives all of
them.
Try it yourself
2 visible tests · 3 hidden testsModel singleton pre-instantiation. Implement
creationOrder(List<String> registrationOrder, Map<String, List<String>> dependencies).
registrationOrder lists every bean name in the order the definitions
were registered; dependencies maps every bean to the beans its
constructor needs, in parameter order.
Visit beans in registration order and "get" each one. Getting a bean
that already exists does nothing. Otherwise mark it as in creation,
get each of its dependencies in order, then create it and append its
name to the result. If you are asked to get a bean that is currently
in creation, stop immediately and return the names created so far
followed by one final element "BeanCurrentlyInCreationException: <name>",
where <name> is the bean that was requested while in creation.
Return the list of created bean names, in creation order.
creationOrder(["webController","orderService","orderRepository"], {"orderService":["orderRepository"],"webController":["orderService"],"orderRepository":[]})creationOrder(["a","b","c"], {"a":[],"b":[],"c":[]})
Sign up to run the tests, check the hidden ones and save your progress. Sign up