Skip to content

Spring IoC: Dependency Injection, Containers, Beans & Autowiring


Goal of this Chapter This is not "here's the @Autowired annotation." The whole chapter answers one question: who decides which concrete object a class gets to work with โ€” the class itself, or something external to it? Every mechanism here (constructor injection, @Qualifier, the IoC container) is a different answer to that one question, and the answer determines whether your code is testable, or hard-wired to concrete classes you can never swap out.

Crisp Definition

Inversion of Control means a class no longer decides how its dependencies are created โ€” an external container creates them and hands them in. Dependency Injection is the specific technique Spring uses to do that handing-in: constructor, setter, or field.


Dependency Injection: Maven's Meaning vs. Spring's Meaning

The phrase "dependency" means something different in each tool:

Maven's "dependency"   โ†’ a dependency on a PACKAGE   (compile/build-time concern)
Spring's "dependency"  โ†’ a dependency on an OBJECT    (runtime concern)
A obj = new A(new B(), new C());

Here, B and C are dependencies of A โ€” this is Spring's sense of the word. Writing new yourself means your code decides exactly which concrete objects A gets. Spring exists to let you write this without ever calling new for B or C yourself.


What's Actually Wrong With new?

Engineering Problem

class OrderService {
    private PaymentGateway gateway = new PaymentGateway();

    void placeOrder() {
        gateway.pay(100);
    }
}

This isn't a style complaint โ€” it's a concrete set of problems:

  1. OrderService is locked to PaymentGateway โ€” no other implementation can ever be substituted.
  2. You cannot replace PaymentGateway without editing OrderService's source.
  3. Testing becomes painful.

Why Testing Breaks

You want to test: does placeOrder() call payment? โ€” not does it actually charge real money? But new PaymentGateway() connects to a real payment system: a real network call, real money deducted. The test becomes slow, unsafe, or outright impossible to run in CI.

Definition โ€” Mocking

Mocking means using a fake object instead of the real one during testing โ€” fake behavior, no real DB, no real payment, no real network.

Why new Blocks Mocking

Because you hardcoded the dependency. new PaymentGateway() says "ONLY use this exact concrete class" โ€” you left no room to swap it for a fake at test time.


The Fix โ€” Dependency Injection (Manual, Before Spring)

Step 1: Remove new

class OrderService {
    private PaymentGateway gateway;

    OrderService(PaymentGateway gateway) {
        this.gateway = gateway;
    }

    void placeOrder() {
        gateway.pay(100);
    }
}

Now OrderService says: "I don't care HOW payment happens. Just give me something that can pay."

Testing Becomes Easy

class FakePaymentGateway extends PaymentGateway {
    void pay(int amount) {
        System.out.println("Fake payment of " + amount);
    }
}

PaymentGateway fake = new FakePaymentGateway();
OrderService service = new OrderService(fake);
service.placeOrder();

No real payment. Fast. Safe. Controlled behavior โ€” this is mocking.

Where Spring Comes In

Doing this manually everywhere doesn't scale. Spring automates it: creates objects, injects dependencies, and lets you swap real โ†’ mock easily in tests.

@Service
class OrderService {
    private final PaymentGateway gateway;

    OrderService(PaymentGateway gateway) {
        this.gateway = gateway;
    }
}

Spring creates PaymentGateway, injects it into OrderService, and in tests injects a mock instead. You never write new.

Mocking in Real Life โ€” JUnit + Mockito

PaymentGateway mockGateway = mock(PaymentGateway.class);
OrderService service = new OrderService(mockGateway);
service.placeOrder();

verify(mockGateway).pay(100);

This checks "did my code behave correctly?" โ€” not "did payment actually happen?"

The Key Mental Switch

Using new                โ†’ "I decide what object to use"
Dependency Injection      โ†’ "I receive what object to use"

That's the whole thing.

One-line interview explanation: "Using new tightly couples classes and makes mocking difficult; dependency injection allows replacing real objects with mocks during testing."


Inversion of Control (IoC)

Engineering Problem

private PaymentGateway gateway = new PaymentGateway();

The new for this dependency lives inside your class โ€” if that component has an issue (fails to construct, needs config you don't have), your class won't even compile or run. Object-creation failures are baked into the class that shouldn't need to care about them.

Definition

Move to constructor injection instead:

private final PaymentGateway gateway;

OrderService(PaymentGateway gateway) {
    this.gateway = gateway;
}

Now the control of object creation sits at the client (caller) side โ€” every failure or exception from constructing the dependency is handled there, not inside OrderService. This is called Inversion of Control: control over how and when a dependency is created is inverted away from the class that uses it. Spring uses IoC extensively โ€” a Spring app doesn't call new for its own beans; the framework does.

Engineering Insight

The name is literal, not clever branding: control that would normally sit inside the dependent class (deciding what to construct, and dealing with construction failures) is inverted โ€” pushed outward to the caller, and ultimately to the framework. Dependency Injection is simply the mechanism Spring uses to perform that inversion.


IoC Container & Beans

Definition โ€” IoC Container

The IoC Container is the Spring runtime that creates objects, manages their lifecycle, and injects dependencies. ApplicationContext is the most commonly used IoC container in Spring.

Definition โ€” Bean

A Bean is a Java object managed by the Spring IoC container. Spring controls its creation, its wiring (dependency injection), and its lifecycle (init, destroy). Typical examples: a database accessor, a network/connection pool, service classes, configuration objects.

Scope & Lifecycle

The container holds bean instances with a defined scope:

  • Default scope: singleton โ€” one instance per ApplicationContext.
  • Other scopes exist (prototype, request, session, ...).

Spring manages when the instance is created, when it's destroyed, and how long it lives.

Interdependent Beans

Beans can depend on other beans, and Spring resolves those dependencies automatically:

OrderService โ†’ PaymentService โ†’ Logger

Spring builds a dependency graph, instantiates beans in the correct order, and injects required dependencies โ€” the same graph-resolution mechanics used elsewhere in Spring's autowiring.

Interview answer: "The Spring IoC container, typically ApplicationContext, manages beans โ€” Java objects whose lifecycle and dependencies are controlled by Spring. These beans are usually singleton-scoped and can depend on each other, with Spring resolving and injecting dependencies automatically."


Spring Boot โ€” Convention Over Configuration

Philosophy

Reduce manual setup, provide sensible defaults, and let the developer override only when needed: "Spring Boot auto-configures common infrastructure based on classpath."

What Auto-Configuration Does

Spring Boot automatically creates beans, based on:

  • classes present on the classpath,
  • existing user-defined beans, and
  • properties.

It uses conditional annotations like @ConditionalOnClass, @ConditionalOnMissingBean, and @ConditionalOnProperty.

Key rule: Boot configures only if you haven't already configured it โ€” it will not override your custom bean.

Starters

<artifactId>spring-boot-starter-web</artifactId>

A starter is a curated dependency bundle โ€” it pulls in required libraries, auto-config support, and sensible defaults, all at once.

How Boot Decides What to Configure

Classpath-driven configuration:

  • Web classes present โ†’ configure DispatcherServlet.
  • JDBC driver present โ†’ configure a DataSource.
  • Jackson present โ†’ configure an ObjectMapper.

Important Clarification

Boot is not "intelligent guessing." It checks conditions, applies configuration only if those conditions match, and fails if a requirement is missing.

Interview one-liner: "Spring Boot uses conditional auto-configuration to create default beans based on the classpath, reducing manual configuration while allowing full override."


Making Objects Discoverable โ€” @Component and the Bootstrap Annotations

Engineering Problem

If Spring needs to know which classes should become beans, a naming convention (<ClassName>Bean?) is fragile โ€” Bean, bean, Been, BEAN all invite typos and inconsistency across a codebase. Something more reliable than string-matching a class name is needed.

Solution โ€” @Component

@Component marks a class as a Spring bean candidate, so Spring can instantiate and manage its object lifecycle.

ApplicationContext = the Name for the IoC Container

Just terminology: when you see ApplicationContext, that is the IoC container instance for your app.

@SpringBootApplication

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class}),
        @Filter(type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class})
    }
)
public @interface SpringBootApplication { ... }
Annotation Responsibility
@SpringBootConfiguration A specialized @Configuration โ€” this class defines Spring bean configuration for the app
@EnableAutoConfiguration Enables Spring Boot's auto-configuration mechanism, based on classpath and existing beans
@ComponentScan Scans the package (and sub-packages) for Spring components (@Component, @Service, ...) and registers them as beans

Ultra-short mental model: @SpringBootApplication = Config + Auto-config + Component scan, in one annotation.

Naming a Bean Explicitly

@Component("thisWillBeTheNameOfThisBean")

Two Gotchas Worth Remembering

1. @Component on an interface or abstract class does nothing. It doesn't throw an error, but it has no effect โ€” and any class implementing/extending that interface/abstract class (or any superclass) does not inherit the annotation. Each concrete class needs its own @Component.

2. SpringApplication.run(...) is what actually performs bean management, injection, and autowiring โ€” a manually new'd object bypasses all of it:

ApplicationContext ctx = SpringApplication.run(MyFirstSpringProjectApplication.class, args);
DatabaseAccessor databaseAccessor = (DatabaseAccessor) ctx.getBean("databaseAccessor");
databaseAccessor.talkToDB();   // works โ€” Spring wired the Logger in

DatabaseAccessor dba = new DatabaseAccessor();
dba.talkToDB();                // โœ˜ NPE โ€” Spring never touched this instance,
                                //   its Logger field was never set

Autowiring only happens for objects the container created. Instantiate the class yourself, and you've opted out of every wiring guarantee Spring would otherwise provide.


Autowiring

Definition

Autowiring is Spring's mechanism for automatically resolving and injecting bean dependencies at runtime. If a bean DBAccessor depends on another bean Logger, Spring โ€” using the application context โ€” locates a suitable Logger bean and injects it into DBAccessor, via constructor, field, or setter injection.

For a dependency to be found, it must itself be a bean โ€” otherwise Spring has nothing in the container to inject.


Field Autowiring

@Component
public class Logger {
    public void log() {
        System.out.println("#######--Logging--#######");
    }
}

@Component
public class DatabaseAccessor {
    @Autowired   // field autowiring
    private Logger logger;

    public void talkToDB() {
        System.out.println("#######--Talkin--To--Database--#######");
        logger.log();
    }
}
@SpringBootApplication
public class MyFirstSpringProjectApplication {
    public static void main(String[] args) throws ClassNotFoundException {
        ApplicationContext ctx = SpringApplication.run(MyFirstSpringProjectApplication.class, args);
        DatabaseAccessor databaseAccessor = (DatabaseAccessor) ctx.getBean("databaseAccessor");
        databaseAccessor.talkToDB();
    }
}
// Output:
// #######--Talkin--To--Database--#######
// #######--Logging--#######

The Drawback

Field autowiring is a ticking time bomb for NullPointerException. When Spring creates the object, everything works fine โ€” but if a developer or test manually instantiates the class with new and forgets to set the hidden dependency, calling a method that uses it throws an NPE. Nothing in the class signature warns you the dependency exists.

Core Design Problem

Field injection:

  • hides required dependencies (nothing visible from outside the class body),
  • breaks immutability (the field can't be final),
  • makes manual testing harder (you need reflection or a framework to set it), and
  • violates the explicit dependency principle.

Interview one-liner: "Field injection hides required dependencies and can cause runtime NullPointerExceptions when objects are instantiated outside the Spring container."


Ambiguous Autowiring โ€” @Qualifier and Named Autowiring

Engineering Problem

When a bean's type is an interface with multiple implementations, Spring has no way to know which one to inject by type alone:

public interface Game {
    void play();
}

@Component
public class Chess implements Game {
    public void play() { System.out.println("#####--Playing--Chess--#####"); }
}

@Component
public class Ludo implements Game {
    public void play() { System.out.println("#####--Playing--Ludo--#####"); }
}

@Component
public class GameManager {
    @Autowired
    private Game game;

    public void manage() {
        game.play();
    }
}
Field game in com.example.demo.GameManager required a single bean, but 2 were found:
- chess
- ludo

Action: Consider marking one of the beans as @Primary, updating the consumer to
accept multiple beans, or using @Qualifier to identify the bean that should be consumed

Fix โ€” @Qualifier

@Component
public class GameManager {
    @Autowired
    @Qualifier("ludo")
    private Game game;

    public void manage() {
        game.play();
    }
}
// Output: #####--Playing--Ludo--#####

Named Autowiring โ€” the Implicit Alternative

The same disambiguation can happen without @Qualifier, purely from the field's name:

@Component
public class GameManager {
    @Autowired
    private Game chess;   // Spring uses reflection to read the field name and matches it to a bean

    public void manage() {
        chess.play();
    }
}
// Output: #####--Playing--Chess--#####

This isn't magic โ€” Spring uses reflection to inspect the field name, and falls back to matching it against bean names when the type alone is ambiguous.

Does @Qualifier Kill Runtime Polymorphism?

No. @Qualifier(...) is only honored by the specific client (GameManager, here) that declares it. A different client can hold its own distinct Game game field with no qualifier, or a different qualifier entirely โ€” each caller's wiring choice is independent, so polymorphism at the Game interface level is untouched; only this particular injection point's choice is pinned.


Setter Autowiring

@Component
public class GameManager {
    private Game game;

    @Autowired
    public void setGame(@Qualifier("chess") Game game) {
        this.game = game;
    }

    public void manage() {
        game.play();
    }
}

When Setter Injection Is Genuinely Useful

1. Optional dependencies โ€” the primary use case:

@Autowired(required = false)
public void setLogger(Logger logger) {
    this.logger = logger;
}

If a Logger bean exists, it's injected; if not, there's no failure โ€” the bean is still created. Constructor injection cannot support optional dependencies this cleanly.

2. Late / reconfigurable dependencies โ€” setter injection allows changing a dependency after object creation, or replacing a bean in advanced scenarios. Less common, but valid.

Cons of Setter Injection

Mutability โ€” gameManager.setLogger(newLogger) means the dependency can change after construction; the object is not immutable, and its state becomes harder to reason about. This violates the "fully initialized after construction" principle.

Potential race conditions (nuance) โ€” in singleton beans (the default scope), Spring creates the bean single-threaded during startup; multiple threads may access it afterward. If the setter is public and something mutates the dependency at runtime, one thread may observe the old reference while another observes the new one โ€” unpredictable behavior. However: Spring itself injects dependencies before the bean is exposed to the application, so this risk only materializes if you mutate the bean later.

Hidden required-dependency risk โ€” if a setter-injected dependency is optional (or the setter is never called), the object may end up partially initialized. Constructor injection prevents this by construction.

Interview one-liner: "Setter injection is useful for optional dependencies or when dependencies may change after object creation, but constructor injection is preferred for mandatory dependencies."


Constructor Autowiring

@Component
public class DatabaseAccessor {
    private final Logger logger;

    @Autowired   // constructor autowiring
    DatabaseAccessor(Logger l) {
        this.logger = l;
    }

    public void talkToDB() {
        System.out.println("#######--Talkin--To--Database--#######");
        logger.log();
    }
}

Spring scans @Component classes, checks whether they have a constructor annotated @Autowired (or a single constructor at all), and injects the dependency there โ€” this is what prevents the field-autowiring NPE risk entirely: the object simply cannot exist without its Logger.

With a Polymorphic Dependency

@Component
public class GameManager {
    private final Game game;

    @Autowired
    GameManager(@Qualifier("chess") Game g) {
        this.game = g;
    }

    public void manage() {
        game.play();
    }
}

Why This Is Better Than Setter Injection

  • Object is fully initialized at creation โ€” no partially constructed state is possible.
  • Dependencies are explicit โ€” everything required is visible just by looking at the constructor signature.
  • Immutability possible โ€” fields can be final.
  • No mutation after construction โ€” setter injection allows late modification; constructor injection doesn't.

When Setter Injection Is Still Acceptable

  • Truly optional configuration.
  • Rare circular-dependency scenarios.
  • Legacy code.

But modern Spring practice defaults to constructor injection.

Senior-Level Mental Model

Constructor Injection Setter Injection
Object is complete, immutable, thread-safe by design Object is configurable but potentially unstable

Senior-level one-liner: "Constructor injection makes dependencies explicit and immutable; optional dependencies can be modeled using Optional<T> without resorting to setter injection."

Optional<T> in Constructor Autowiring

Combining a mandatory (qualified) dependency with a genuinely optional one, without falling back to setter injection at all:

@Component
public class GameManager {
    private final Game game;
    private final Logger logger;

    @Autowired
    GameManager(@Qualifier("chess") Game game, Optional<Logger> logger) {
        this.game = game;
        this.logger = logger.orElse(null);
    }

    public void manage() {
        game.play();
        if (logger != null) {
            logger.log();
        }
    }
}
ApplicationContext ctx = SpringApplication.run(MyFirstSpringProjectApplication.class, args);
GameManager gameManager = (GameManager) ctx.getBean("gameManager");
gameManager.manage();

GameManager gm = new GameManager(new Ludo(), Optional.empty());
gm.manage();

// Output:
// #####--Playing--Chess--#####
// #####--Playing--Ludo--#####

Engineering Insight

Constructor injection is the same reasoning as final and immutability applied to object wiring: a final field set only in the constructor can't drift after construction, which is exactly what makes a bean safe to share across threads without synchronization. Setter injection reopens that door deliberately โ€” acceptable only when the dependency is genuinely optional or reconfigurable, never as the default.


Interview Q&A

How does "dependency" differ in meaning between Maven and Spring?

Maven's dependency is a dependency on a package โ€” a compile/build-time concern. Spring's dependency is a dependency on an object โ€” a runtime concern, resolved and injected by the IoC container.

What's concretely wrong with writing new PaymentGateway() inside OrderService?

It locks OrderService to one concrete implementation, makes that implementation impossible to swap, and blocks mocking in tests โ€” since new always constructs the real object, tests using it would hit a real network call and real side effects.

What is mocking, and why does new block it?

Mocking is substituting a fake object for the real one during a test. new blocks it because it hardcodes exactly which concrete class gets constructed โ€” there's no injection point left for a caller to substitute a fake.

What is Inversion of Control?

Moving the responsibility for creating and configuring a dependency out of the class that uses it, and into an external caller or framework โ€” so failures in constructing that dependency are handled at the client/framework side, not baked into the dependent class.

What is a Spring Bean, and what does the IoC container manage about it?

A Bean is a Java object whose creation, dependency wiring, and lifecycle (init/destroy) are controlled by the Spring IoC container (ApplicationContext), rather than by application code calling new.

What's the default Spring bean scope, and what does it mean?

Singleton โ€” one instance per ApplicationContext. Note this is scoped to the container, not the JVM: multiple ApplicationContexts can each hold their own separate instance.

What does Spring Boot's "convention over configuration" actually check before creating a bean?

Conditions โ€” via annotations like @ConditionalOnClass, @ConditionalOnMissingBean, @ConditionalOnProperty โ€” based on what's on the classpath, what beans already exist, and configured properties. It's condition-checking, not "intelligent guessing," and it never overrides a bean you've already defined yourself.

Why does @Component on an interface have no effect?

Because Spring instantiates concrete classes, and annotations are not inherited by implementing/extending classes โ€” each concrete class needs its own @Component (or equivalent stereotype annotation).

If you call new DatabaseAccessor() yourself instead of getting it from ApplicationContext, what happens to its @Autowired field, and why?

It stays unset, because autowiring only happens for objects the Spring container itself creates during SpringApplication.run(...). A manually constructed instance never goes through that wiring process, so calling a method relying on the field throws NullPointerException.

What's the main risk with field autowiring specifically?

If the object is ever manually instantiated outside the Spring container (in a test, or by a developer forgetting the framework manages it), the field is never set โ€” and using it throws an NPE with no compile-time warning, because the dependency requirement is invisible from the constructor.

If two beans implement the same interface, how does Spring know which to inject?

It doesn't, by type alone โ€” you either disambiguate explicitly with @Qualifier("beanName"), or rely on Named Autowiring, where Spring uses reflection to match the field/parameter name itself against a bean name.

Does using @Qualifier on one injection point break polymorphism elsewhere?

No โ€” @Qualifier is only honored at the specific injection point that declares it. Other clients of the same interface can hold entirely different, unqualified or differently-qualified references; each wiring decision is local to its own call site.

When is setter injection actually the right choice over constructor injection?

For genuinely optional dependencies (@Autowired(required = false)), for dependencies that may legitimately change after construction, for some circular-dependency workarounds, or in legacy code โ€” not as a general default.

Why is constructor injection considered the modern Spring default?

It guarantees the object is fully initialized at creation with no partially-constructed state, makes every dependency explicit in the constructor signature, allows final fields (immutability), and forbids any mutation after construction โ€” all of which setter injection allows to happen.

How can a constructor-injected bean still support one optional dependency without falling back to setter injection?

By taking Optional<T> as a constructor parameter โ€” Spring injects Optional.empty() if no matching bean exists, and the constructor resolves it explicitly (e.g. logger.orElse(null)), keeping the class immutable and its optionality visible in the signature.


โŒ Dependency Injection just means "Spring creates my objects for me" โ€” it's a convenience feature.

โœ” It's specifically what makes classes swappable and testable โ€” the actual engineering payoff is decoupling what a class needs from which concrete implementation satisfies it, which is what makes mocking possible in the first place.


โŒ A Spring singleton bean means one instance exists in the whole JVM.

โœ” It means one instance per ApplicationContext (container) โ€” multiple containers in the same JVM can each hold their own instance.


โŒ Putting @Component on an interface makes every implementation of it a bean automatically.

โœ” @Component is not inherited โ€” every concrete implementing class needs its own @Component (or equivalent) annotation; the one on the interface has no effect at all.


โŒ If a class is annotated @Component, calling new on it manually still gets you a fully-wired object.

โœ” Autowiring only happens for objects the Spring container itself constructs during SpringApplication.run(...). A manually new'd instance never goes through that process โ€” its @Autowired fields stay unset.


โŒ @Qualifier on one field means every use of that interface across the app now resolves to that specific bean.

โœ” @Qualifier only applies to the specific injection point that declares it โ€” other clients of the same interface are free to wire a different implementation, or none at all.


โŒ Setter injection is strictly worse than constructor injection and should never be used.

โœ” It's the correct tool specifically for optional or late-changing dependencies (@Autowired(required = false)) โ€” the objection is to using it as the default for mandatory dependencies, not to its existence.


What You Should Remember Forever

Maven "dependency"    โ†’ a package (compile/build-time)
Spring "dependency"    โ†’ an object (runtime)

new                    โ†’ "I decide what object to use"  (tight coupling,
                          blocks mocking)
Dependency Injection    โ†’ "I receive what object to use" (swappable, testable)

Inversion of Control    โ†’ object-creation responsibility moves from the
                           dependent class to an external caller/framework

IoC Container (ApplicationContext) โ†’ creates beans, injects dependencies,
                                       manages lifecycle
Bean scope (default)                โ†’ singleton, one per ApplicationContext

@Component               โ†’ makes a class a bean candidate; NOT inherited,
                            has no effect on interfaces/abstract classes
SpringApplication.run()  โ†’ the only path through which autowiring happens;
                            manual `new` bypasses it entirely

Field injection    โ†’ hides deps, breaks immutability, NPE risk if `new`'d manually
Setter injection    โ†’ mutable, but the right fit for optional/late-bound deps
Constructor injection โ†’ explicit, immutable, no NPE risk โ€” the modern default

Multiple beans, same interface โ†’ disambiguate with @Qualifier or
                                   Named Autowiring (field-name match)
Optional<T> in a constructor    โ†’ models an optional dependency WITHOUT
                                   giving up constructor injection

Stage 2 ยท Week 1 Checklist

  • [x] Explain why constructing dependencies with new blocks mocking and unit testing
  • [x] Explain Inversion of Control in your own words, using the "control moves to the caller" framing
  • [x] Explain what an IoC container and a Spring Bean are, and the default bean scope
  • [x] Explain Spring Boot's convention-over-configuration philosophy and why it never overrides your own beans
  • [x] Explain why @Component on an interface has no effect
  • [x] Explain why manually calling new on a @Component-annotated class produces an unwired object
  • [x] Explain the difference between field, setter, and constructor autowiring, and rank them by safety
  • [x] Explain how @Qualifier and Named Autowiring resolve ambiguous bean injection
  • [x] Explain why constructor injection is the modern default, and when setter injection is still the right call
  • [x] Explain how Optional<T> lets constructor injection model an optional dependency