Enterprise 1 — Dependency Injection
In one line
Dependency injection (DI) means a class receives the objects it needs, usually through its constructor, instead of creating them itself. It makes dependencies visible, lets tests pass fakes, and lets a container such as Spring wire the whole application at startup.
Think of it like this
A chef who is handed ingredients by the kitchen manager instead of driving to the market personally. The chef cooks the same way whether the tomatoes came from the local shop or a test kitchen's fake supply, and the manager decides where supplies come from.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Dependency
- An object a class needs to do its job, such as a repository or a payment gateway.
- Injection
- Handing a dependency to an object from outside instead of letting it create the dependency itself.
- Composition root
- The single place (startup code or the container) where concrete implementations are chosen and wired together.
- Bean
- In Spring, an object created and managed by the container.
Step by step
01The problem: hidden, hard-wired dependencies
Tiffin's CheckoutService created its own RazorpayGateway and JdbcOrderStore inside the constructor. Unit tests therefore hit the real payment sandbox and a real database, ran slowly and failed whenever the sandbox was down. Switching to a second payment provider meant editing the service.
02The fix: ask for dependencies in the constructor
The service now declares two interfaces in its constructor. Production code passes real implementations; the test passes in-memory fakes. The service's logic didn't change at all.
import java.util.*;
interface PaymentGateway { boolean charge(String orderId, long paise); }
interface OrderStore { void save(String orderId, String status); String status(String orderId); }
final class CheckoutService {
private final PaymentGateway payments; // what it needs is visible in one place
private final OrderStore orders;
CheckoutService(PaymentGateway payments, OrderStore orders) { // constructor injection
this.payments = payments;
this.orders = orders;
}
String checkout(String orderId, long paise) {
String status = payments.charge(orderId, paise) ? "PAID" : "PAYMENT_FAILED";
orders.save(orderId, status);
return status;
}
}
// test doubles: no network, no database
final class FakeGateway implements PaymentGateway {
public boolean charge(String id, long paise) { return paise <= 100_000; } // decline above Rs 1000
}
final class InMemoryOrders implements OrderStore {
private final Map<String, String> rows = new HashMap<>();
public void save(String id, String s) { rows.put(id, s); }
public String status(String id) { return rows.get(id); }
}03Letting Spring do the wiring
In a Spring Boot application, the implementations are annotated @Component (or @Repository, @Service) and the service keeps its plain constructor. Spring sees that CheckoutService needs a PaymentGateway and an OrderStore, finds the beans that implement them, and passes them in. A single constructor doesn't even need @Autowired.
When several beans implement the same interface (two gateways), choose with @Primary, @Qualifier("razorpay"), or inject all of them as Map<String, PaymentGateway> and pick at runtime (Strategy, Behavioral 1).
Try it yourself
- 1
Test the service with fakes
No network or database needed: the test chooses the dependencies.
terminal$ jshell> /open Checkout.javajshell> var orders = new InMemoryOrders()jshell> var svc = new CheckoutService(new FakeGateway(), orders)jshell> svc.checkout("ord-1", 45_000)jshell> svc.checkout("ord-2", 250_000)jshell> orders.status("ord-2")── expected output ──$4 ==> "PAID"$5 ==> "PAYMENT_FAILED"$6 ==> "PAYMENT_FAILED"
In your stack
- →
Spring (
@Component, constructor injection,@Configuration+@Beanfor third-party classes) and Jakarta CDI (@Inject) are the common Java containers; Google Guice and Dagger are lighter alternatives. Since Spring 4.3, a class with a single constructor is injected without@Autowired.
Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Field injection hides a missing dependency
A developer switches CheckoutService to @Autowired private PaymentGateway payments; (field injection), and a unit test creates it with new CheckoutService().
Myth vs fact
Myth
Dependency injection requires a framework.
Fact
DI is just passing dependencies in. A main method that builds objects and passes them to constructors is DI; Spring automates it for large applications.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Watch for constructors with many parameters: they signal a class with too many responsibilities, which DI makes visible rather than causing. Circular dependencies (A needs B, B needs A) are a design smell; Spring Boot 2.6+ refuses them by default.
Remember this
- 1
Without DI, a class calls
new PaymentGateway()orGateway.getInstance()inside itself. It's tied to that exact implementation, and a test can't replace it. - 2
With constructor injection, the class declares what it needs as constructor parameters (preferably interfaces). Whoever creates it (a test, a
mainmethod, or a framework) chooses the implementations. - 3
A DI container (Spring's
ApplicationContext) builds the object graph for you: it finds classes marked as components, creates them once (singletons by default), and passes them into constructors that need them. - 4
DI is the practical form of the Dependency Inversion Principle (Topic 1.2): high-level code depends on abstractions, and the concrete choice is made at the edge of the application.
- 5
Prefer constructor injection over field injection (
@Autowiredon fields): dependencies are explicit, objects can be immutable, and tests can construct them without the container.
Explain it without notes
Why does constructor injection make code easier to test?
What is a composition root?
Practice
Refactor a class that calls new SmsClient() inside a method so it can be unit-tested.
Two notification channels implement Notifier. Show how Spring can inject the right one by name.
Trade-offs
- ↔
Testable, swappable, explicit dependencies, versus more interfaces and, with a container, startup magic that must be understood when wiring fails.
Done when you can
I can refactor a class to constructor injection and test it with fakes.
I can explain how Spring chooses which bean to inject.