01-spring-foundations

IoC and Dependency Injection: Who Builds Your Objects

Spring's container answers one question: who calls new? Inversion of control, the three injection styles and why only one is safe, and what bean scopes change.

October 8, 2026
springspring-bootiocdependency-injectionbeansbean-scopefundamentals

The Only Question That Matters

Strip away the annotations, the starters and the twelve years of accumulated vocabulary, and Spring is answering a single question:

Who is responsible for creating your objects and wiring them together?

In plain Java, you are. A service needs a repository, so the service writes new JdbcOrderRepository(dataSource). That line looks harmless, and it is — once. The trouble is what it quietly decides.

java
public class OrderService {
    private final OrderRepository repository = new JdbcOrderRepository(/* ...a DataSource from where? */);
    private final PaymentGateway gateway = new StripeGateway("sk_live_...");
}

OrderService now knows which implementation of a repository it uses, how to build it, and what credentials the payment gateway needs. It cannot be tested without a live database and a real Stripe key. Swapping the gateway means editing this class. The class has two jobs: doing order logic, and assembling its own collaborators.

Inversion of control (IoC) inverts exactly that one responsibility. Your code stops creating its collaborators and instead declares what it needs. Something else — the container — builds the objects and hands them over.

💡

IoC is the principle: control over object construction moves out of your class. Dependency injection is the mechanism Spring uses to implement it — the container injects dependencies into your object. People use the two terms interchangeably in conversation; it's worth keeping them straight because IoC covers other inversions too, like the framework calling your controller method rather than your code calling the framework.

The Container, and the Two Names for It

Spring's container is the thing that holds your objects. An object managed by the container is called a bean — that is the entire meaning of the word. Not a special type, not an interface to implement. A bean is just an object whose lifecycle Spring owns.

You will meet two names for the container itself:

  • BeanFactory — the base interface. Can hold bean definitions and produce beans on request. Deliberately minimal.
  • ApplicationContext — a BeanFactory plus everything you actually want: event publishing, resource loading, message resolution for i18n, and automatic detection of infrastructure beans like BeanPostProcessors.

In practice you always have an ApplicationContext. BeanFactory matters for understanding the layering, not for daily work.

The two-stage split in that diagram is worth holding onto. Spring first reads definitions — a catalogue of what could be built and what each thing needs — and only then instantiates. That ordering is what lets the container resolve a dependency graph it hasn't built yet, and it's why a missing or ambiguous bean is reported at startup rather than at the moment of first use.

The Same Service, Inverted

Here is OrderService written for the container:

java
@Service
public class OrderService {
 
    private final OrderRepository repository;
    private final PaymentGateway gateway;
 
    public OrderService(OrderRepository repository, PaymentGateway gateway) {
        this.repository = repository;
        this.gateway = gateway;
    }
 
    public Order place(NewOrder request) {
        gateway.charge(request.amount());
        return repository.save(request.toOrder());
    }
}

Three things changed, and each one is a concrete gain:

  1. @Service registers it. Spring's component scan finds the class and creates one instance. (@Service, @Repository and @Controller are all @Component with a role label attached — functionally near-identical, but they document intent and @Repository additionally translates persistence exceptions.)
  2. The constructor declares needs. OrderService asks for an OrderRepository. It no longer knows or cares that the implementation talks to Postgres.
  3. Both fields are final. The object cannot exist in a half-built state, and it's safe to share across threads.

Testing is now ordinary Java — no container required:

java
var service = new OrderService(new InMemoryOrderRepository(), new FakeGateway());

That last point is the one people underrate. A class designed for injection is a class you can construct by hand. If writing a unit test for a Spring bean feels like it needs Spring, something has usually gone wrong in how it takes its dependencies.

Three Injection Styles, One Recommendation

Spring can inject through a constructor, a setter, or directly into a field. They are not equivalent.

Constructor injection — use this

java
@Service
public class OrderService {
    private final OrderRepository repository;
 
    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }
}

Since Spring 4.3, a class with a single constructor needs no @Autowired annotation at all — the container uses that constructor. Most modern Spring code has no @Autowired anywhere in it.

Dependencies are mandatory, fields are final, and the object is fully initialised the moment it exists.

Setter injection — for genuinely optional dependencies

java
@Service
public class ReportService {
    private MetricsCollector metrics = MetricsCollector.NOOP;
 
    @Autowired(required = false)
    public void setMetrics(MetricsCollector metrics) {
        this.metrics = metrics;
    }
}

Legitimate when a dependency is truly optional or needs to be reconfigurable after construction. That is a narrow case; the sensible default is still the constructor.

Field injection — avoid

java
@Service
public class OrderService {
    @Autowired private OrderRepository repository;  // don't
}

It is the shortest to type, which is why it spread. The costs:

  • The field can't be final, so the object is mutable and briefly exists un-wired.
  • new OrderService() compiles and gives you an instance whose field is null. Unit tests must use reflection or start a context.
  • Dependencies become invisible. A constructor with nine parameters is an obvious design smell; nine @Autowired fields hide in a tidy column.
⚠️

Field injection also conceals circular dependencies. If A needs B and B needs A, constructor injection fails loudly at startup — Spring cannot construct either one first. With field injection the cycle can be papered over, and you keep a design knot that will eventually surface as a confusing initialisation-order bug. Spring Boot has refused circular references by default since 2.6; treat that failure as useful information, not an obstacle to configure away with spring.main.allow-circular-references.

Check yourself

A class has a single constructor taking two dependencies, and no @Autowired annotation anywhere. What does Spring do when it creates this bean?

When Two Beans Fit One Slot

Ask for an OrderRepository when two implementations are registered and startup fails with NoUniqueBeanDefinitionException. The container is telling you your declaration was ambiguous. Three ways to resolve it:

java
// 1. @Primary on the default choice — at the definition site
@Repository @Primary
public class JdbcOrderRepository implements OrderRepository { }
 
// 2. @Qualifier at the injection site — names which one you want
public OrderService(@Qualifier("cachingOrderRepository") OrderRepository repository) { }
 
// 3. Inject all of them — Spring populates the collection
public OrderAudit(List<OrderValidator> validators) { }

The third form is more useful than it first appears. Asking for a List<T> gives you every bean of that type, which is how plugin-style designs work in Spring: add a new @Component implementing OrderValidator and it joins the list with no change to the consuming class. Add @Order to control sequence, or use Map<String, OrderValidator> to get them keyed by bean name.

Bean Scope: How Many Instances Exist

Every bean definition has a scope — how many instances the container creates and how long each lives.

ScopeInstancesLifetimeTypical use
singletonOne per containerApplicationThe default. Services, repositories, clients
prototypeA new one per request for itCaller'sStateful helpers, builders
requestOne per HTTP requestThe requestPer-request context (web apps only)
sessionOne per HTTP sessionThe sessionPer-user state (web apps only)
applicationOne per ServletContextThe appRare

Two misreadings are common enough to call out.

"Singleton" means one per container, not one per JVM. It is not the Gang of Four singleton pattern with a static instance. Two ApplicationContexts in the same JVM — routine in tests — each get their own.

Singleton scope is a decision about state. One instance serves every concurrent request, so a singleton bean with mutable instance fields is a data race:

java
@Service
public class BrokenTotaliser {
    private BigDecimal runningTotal = BigDecimal.ZERO;  // shared across every request
 
    public BigDecimal add(BigDecimal amount) {
        runningTotal = runningTotal.add(amount);  // two threads, lost updates
        return runningTotal;
    }
}

Keep singleton beans stateless: dependencies in final fields, per-call data in method parameters and locals. This is the overwhelmingly common pattern and the reason it's the default — a stateless service is safe to share.

✅

Injecting a prototype bean into a singleton does not do what people expect. The singleton is wired once, so it holds one prototype instance forever — the scope is effectively ignored. When you need a fresh instance per call, inject ObjectProvider<T> and call getObject() at the point of use.

Check yourself

A singleton @Service stores the current user's ID in a private field during each request, so helper methods can read it without passing it around. Under load, users occasionally see each other's data. Why?

What This Buys You

The container is often introduced as a convenience — less new, less plumbing. That undersells it. The real payoff is that something outside your classes understands the whole object graph. Once that's true:

  • Testing becomes substitution. Pass a fake in the constructor; no framework needed.
  • Cross-cutting behaviour becomes possible. Because Spring creates your beans, it can wrap them in proxies. @Transactional, @Cacheable, @Async and method security all work because the container controls instantiation. You cannot retrofit that onto objects you built with new.
  • Configuration becomes a seam. Swapping an implementation per environment is a bean definition change, not an edit to the consuming class.

That second point is the hinge for everything ahead. Nearly every Spring feature that feels magical is the container wrapping your bean in a proxy at creation time.

The Mental Model, Restated

  1. A bean is just an object the container owns. No special type required.
  2. Declare dependencies in the constructor. Mandatory, final, testable, and cycles fail loudly.
  3. @Component and friends say "register me"; constructor parameters say "I need these".
  4. Singleton is the default and means one per container — so keep those beans stateless.
  5. The container owning instantiation is what enables proxies, and proxies are what enable transactions, caching and security.

What's Next

You now know how the container builds and wires beans once it has definitions. The open question is where those definitions come from — because you never wrote a @Bean method for a DataSource, an ObjectMapper or an embedded web server, and yet all three exist in a running Spring Boot app. The next guide takes apart @SpringBootApplication and follows auto-configuration from a JAR on the classpath to a bean in your context.