Hello! Welcome to the first lesson of our second module, "Spring Core & Dependency Injection."
In our last lesson, we concluded our journey through Java web foundations by examining how Spring's DispatcherServlet masterfully implements the Front Controller pattern. We saw how it delegates the work of handling requests to specialized components. This principle of delegating responsibility to an external entity is a perfect lead-in to today's topic.
This lesson addresses a foundational learning outcome for the entire course: Explain the core concepts of Dependency Injection (DI) and Inversion of Control (IoC). These principles are the bedrock of the Spring Framework and modern software engineering. Understanding them is not just about using Spring effectively; it's about writing code that is more modular, flexible, and testable.
1. The Problem: Tightly Coupled Components
Before we can appreciate the solution, we must first understand the problem. In object-oriented programming, classes often need to work together to accomplish a task. When one class needs another class to do its job, we say it has a dependency.
For example, a Car class needs an Engine class to function. The Car depends on the Engine.
The simplest way to satisfy this dependency is to have the Car class create its own Engine instance:
public class Engine {
public void start() {
System.out.println("Engine starting...");
}
}
public class Car {
private Engine engine;
public Car() {
// The Car class is responsible for creating its own dependency.
this.engine = new Engine();
}
public void drive() {
engine.start();
System.out.println("Driving...");
}
}
This seems straightforward, but it introduces a problem called tight coupling. The Car class is now permanently tied to one specific implementation of Engine. What if we want a V8Engine or an ElectricEngine? We would have to change the Car class itself. This makes our code rigid and difficult to test or extend.
To explore this concept further, let's watch two short video clips that define dependencies and highlight the issues with this "traditional" approach of using the new keyword inside a class.
Spring Tutorial - 2 - Dependency Injection & Inversion of Control without Spring
First, let's get a clear definition of what a dependency is in the context of software components. This clip from a Kody Simpson tutorial provides excellent analogies.
Watch from 01:01 to 02:49. Focus on the 'Vehicle depends on Engine' and 'Library Service depends on Book Repository' examples.
Dependency Injection Explained
Now, let's dive into the problems this tight coupling causes, especially concerning testing and object management. This video from Amigoscode clearly explains why using the new keyword can be problematic.
Watch from 01:59 to 05:51. Pay attention to the discussion about why instantiating dependencies directly makes unit testing difficult and can lead to inefficient object creation.
2. The Principle: Inversion of Control (IoC)
The solution to tight coupling is to "invert the control."
- Traditional Control: Our
Carclass controls the creation and management of itsEnginedependency. - Inverted Control: We delegate the control of creating and managing the
Engineto an external entity. This entity is often called a container or a framework.
Inversion of Control (IoC) is a design principle where control over object creation and lifecycle is transferred from your application code to a container.
Think of it this way: instead of your code actively reaching out to create and grab its dependencies, the container creates the objects and hands them to your code when needed. This is sometimes called "The Hollywood Principle": "Don't call us, we'll call you."

Let's get a more formal definition from a well-respected source for Java developers, Baeldung.
Inversion of Control and Dependency Injection with Spring
This article, 'Inversion of Control and Dependency Injection with Spring', provides a clear, concise definition of IoC.
Read Section 2, 'What Is Inversion of Control?'. Focus on the contrast between traditional programming and the IoC approach, and note the listed advantages like decoupling and easier testing.
3. The Pattern: Dependency Injection (DI)
So, how does a container actually give our Car class its Engine? It uses a pattern called Dependency Injection (DI).
Dependency Injection is a design pattern that implements IoC. Instead of an object creating its dependencies, the dependencies are "injected" (passed) into the object from an outside source.
Let's refactor our Car example to use DI. We will use the most common type, constructor injection.
public class Car {
private Engine engine;
// The dependency is now "injected" through the constructor.
public Car(Engine engine) {
this.engine = engine;
}
public void drive() {
engine.start();
System.out.println("Driving...");
}
}
Notice the Car class no longer uses the new keyword to create its Engine. It simply declares that it needs an Engine in its constructor. It's now the responsibility of some other part of our application (the "assembler" or IoC container) to create an Engine and pass it to the Car when a Car is created.
// In some other part of the application (e.g., a main method or a container)
Engine myEngine = new Engine();
Car myCar = new Car(myEngine); // The dependency is injected here!
myCar.drive();
This small change has a huge impact. Our Car class is now loosely coupled from the Engine implementation. We can easily give it a V8Engine or an ElectricEngine as long as they adhere to the same Engine contract (e.g., an Engine interface).

Let's watch a detailed demonstration of refactoring a tightly coupled application to use manual DI. This will solidify your understanding of how it works in practice, even without a framework like Spring.
Spring Tutorial - 2 - Dependency Injection & Inversion of Control without Spring
This video from Kody Simpson walks through a complete example, starting with a tightly coupled design and refactoring it to use Dependency Injection. It also shows the power of using interfaces to achieve swappable components.
Please watch from 13:36 to 23:39. The first part (until 17:28) shows how to refactor a RestaurantService to use constructor injection. The second part (from 17:28) introduces an interface (MessageService) to demonstrate how DI allows you to easily swap between different implementations (EnglishMessageService and SpanishMessageService). This is a key benefit!
Test your understanding!
You are building an e-commerce application. You have a PaymentProcessor class that handles charging the customer. Initially, it's built to use a StripeService.
// Tightly coupled implementation
public class PaymentProcessor {
private StripeService stripeService;
public PaymentProcessor() {
this.stripeService = new StripeService(); // Tightly coupled!
}
public void processPayment(double amount) {
stripeService.charge(amount);
}
}
Your manager now says you also need to support payments via a PayPalService. How would you refactor PaymentProcessor using the principles of DI to accommodate this new requirement without modifying the PaymentProcessor class every time a new payment method is added?
Show answer
The best approach is to depend on an abstraction (an interface) rather than a concrete class.
-
Create an Interface: First, define a
PaymentServiceinterface that captures the common behavior.public interface PaymentService { void charge(double amount); } -
Implement the Interface: Have both
StripeServiceandPayPalServiceimplement this interface.public class StripeService implements PaymentService { @Override public void charge(double amount) { System.out.println("Charging $" + amount + " using Stripe."); } } public class PayPalService implements PaymentService { @Override public void charge(double amount) { System.out.println("Charging $" + amount + " using PayPal."); } } -
Refactor the Class to Use Injection: Change
PaymentProcessorto accept any object that implementsPaymentServicethrough its constructor.// Loosely coupled implementation public class PaymentProcessor { private PaymentService paymentService; // Depends on the interface! public PaymentProcessor(PaymentService paymentService) { this.paymentService = paymentService; // Dependency is injected. } public void processPayment(double amount) { paymentService.charge(amount); } }
Now, the PaymentProcessor is completely decoupled from the specific payment implementation. You can easily switch between them when creating an instance of PaymentProcessor.
PaymentService stripe = new StripeService();
PaymentProcessor processor1 = new PaymentProcessor(stripe);
processor1.processPayment(100.0); // Uses Stripe
PaymentService paypal = new PayPalService();
PaymentProcessor processor2 = new PaymentProcessor(paypal);
processor2.processPayment(50.0); // Uses PayPal
Conclusion
You've just grasped the most important concepts that underpin the Spring Framework. By understanding the "why" behind these principles, you'll be much better equipped to use the framework effectively.
Key Takeaways:
- Tight Coupling, where a class creates its own dependencies using
new, makes code rigid, hard to test, and difficult to maintain. - Inversion of Control (IoC) is a principle that transfers the responsibility of object creation and management from your code to an external container.
- Dependency Injection (DI) is the primary pattern used to achieve IoC. Dependencies are passed into an object from an external source (e.g., via a constructor) rather than being created internally.
- The main benefits of this approach are loose coupling, high modularity, and vastly improved testability, as seen in the ability to swap implementations easily.
What's Next?
So far, we've seen how to perform DI manually. In the next lesson, we will see how the Spring IoC container automates this entire process. We will learn how to tell Spring about our components and their dependencies so it can wire up our application for us. We'll start by looking at how to configure beans using Java-based configuration with @Configuration and @Bean.