Inversion of Control (IoC)
Inversion of Control (IoC)
If you have written plain Java for any length of time you have written code like this: a class decides what it needs, creates it, and uses it. That arrangement is natural but it ties your code in knots the moment you need to swap an implementation, test in isolation, or reuse a component in a different context. Inversion of Control (IoC) is the design principle that untangles those knots by flipping who is responsible for creating dependencies.
The Classic Problem: Hard-Wired Dependencies
Consider a simple order-processing service:
What goes wrong here?
- Untestable in isolation. Every time you instantiate
OrderServicein a test, you get a realStripeGateway— meaning real HTTP calls and real charges. - Change is painful. Switching from Stripe to PayPal requires editing
OrderService, which should not care about payment-provider choice. - Hidden coupling. The class knows which concrete types it needs. That knowledge is scattered across your codebase and must be updated in every class that creates its own dependencies.
The IoC Flip
IoC says: do not create your dependencies — declare them and let someone else provide them. The simplest form is constructor injection:
OrderService now depends on interfaces, not concrete classes. It no longer knows — or cares — whether gateway is a Stripe adapter, a PayPal adapter, or a stub you wrote in a test. The caller (or the IoC container) decides.
main method wire objects together. Spring just automates and scales that wiring.
Three Ways to Inject Dependencies
IoC is delivered through Dependency Injection (DI), which has three common styles:
-
Constructor injection — dependencies are final, set once, always valid. This is the style Spring recommends and the one you should reach for by default.
// Preferred public OrderService(PaymentGateway gw, NotificationService ns) { ... }
-
Setter injection — useful for optional dependencies or when you need to swap a collaborator after construction. More flexible, but the object can exist in a half-configured state.
public void setGateway(PaymentGateway gw) { this.gateway = gw; }
-
Field injection — Spring injects directly into a private field via reflection. Concise to write, but makes the dependency invisible in the constructor, complicates testing, and is generally discouraged in production code.
@Autowired private PaymentGateway gateway; // avoid in production classes
final, and lets you instantiate the class in a plain JUnit test without a Spring context at all — just pass mock objects to the constructor.
Manual Wiring vs. Container Wiring
To see the full picture, wire the object graph by hand first:
This works. But as the graph grows to dozens of objects — each with its own dependencies — the wiring code becomes hundreds of lines. You also have to manage object lifetimes (singleton vs. prototype) and handle circular dependencies yourself. This is exactly the problem a Spring ApplicationContext solves.
What the Container Adds
Spring's IoC container reads your configuration (annotations, Java config, or XML), builds every object, injects every dependency, and manages each object's lifecycle. From that point on you ask the container for a fully-wired object rather than calling new:
The container does the same wiring you did in main(), but automatically, based on your declarations. It also enforces scope (one OrderService shared across the application, or a new one per HTTP request), and it calls lifecycle callbacks (@PostConstruct / @PreDestroy) so your components can initialise and clean up cleanly.
Why This Matters in Practice
- Testability. Because
OrderServiceaccepts its dependencies through the constructor, a unit test can pass in aMockPaymentGateway— no Spring context, no network, no database, tests run in milliseconds. - Replaceability. Switching from
EmailNotifiertoSmsNotifieris a one-line change in configuration, not a search-and-replace across every class that creates a notifier. - Single Responsibility. Business logic classes are freed from the accidental complexity of object creation. They do one thing: their job.
- Composition over inheritance. Behaviour is composed by combining injected collaborators rather than by deep class hierarchies — far easier to reason about and extend.
new ever. Value objects — a Money record, an OrderId wrapper, a DTO — have no collaborators and are fine to construct with new. Reserve IoC for services and infrastructure components that have dependencies and need to be swapped or tested in isolation.
IoC in the Real World: The Hollywood Principle
IoC is sometimes described by the Hollywood Principle: "Don't call us, we'll call you." In traditional code, your class calls a library. With IoC, the framework calls your code — it invokes your business methods at the right time after it has assembled the object graph. You supply the pieces; the framework orchestrates them. This inversion is what gives Spring its power and is why the principle is named what it is.
Summary
IoC reverses the responsibility for creating and connecting objects: instead of a class reaching out to instantiate its own dependencies, the dependencies are supplied from outside. Constructor injection is the preferred mechanism. Manual wiring proves the concept works; a Spring ApplicationContext automates it at scale. The payoff is code that is genuinely testable in isolation, straightforward to reconfigure, and aligned with the Single Responsibility Principle. The next lesson examines the Spring container itself — how it reads your declarations and manages the lifecycle of every object it controls.