85. Dependency Injection Basics
Dependency Injection (DI) supplies objects with their collaborators from outside rather than creating them internally โ making code testable, loosely coupled, and easy to reconfigure; constructor injection is the preferred form.
Java 21 (core language)Explanation
Without DI: new OrderService() calls new PaymentGateway() inside โ tightly coupled, untestable without the real gateway. With DI: new OrderService(paymentGateway) โ the caller supplies the collaborator. In tests, you supply a mock/fake; in production, the real implementation.
Constructor injection (preferred): dependencies passed as constructor parameters, stored in final fields. The class is fully initialised by the constructor, dependencies are explicit, and the class is easily tested with a mock. This is what frameworks like Spring and Quarkus do under the hood.
Field injection (avoid): @Autowired private PaymentGateway gw; โ hidden dependency, untestable without the DI container, and the field cannot be final. Anti-pattern in test-driven code.
Method injection: dependency passed to a specific method rather than the constructor โ useful for optional or per-call dependencies.
Example
import java.util.List;
import java.util.UUID;
public class DependencyInjection {
// Interfaces โ the dependency contracts
interface PaymentGateway { boolean charge(String userId, double amount); }
interface NotificationService { void notify(String userId, String message); }
// Implementations
static class StripeGateway implements PaymentGateway {
@Override
public boolean charge(String userId, double amount) {
System.out.printf("Stripe: charging %s $%.2f%n", userId, amount);
return true; // simulate success
}
}
static class EmailNotifier implements NotificationService {
@Override
public void notify(String userId, String message) {
System.out.println("Email to " + userId + ": " + message);
}
}
// DI via constructor injection: dependencies are explicit and final
static class OrderService {
private final PaymentGateway payment;
private final NotificationService notifier;
// Constructor injection โ testable with any implementation
OrderService(PaymentGateway payment, NotificationService notifier) {
this.payment = payment;
this.notifier = notifier;
}
String placeOrder(String userId, double amount) {
String orderId = UUID.randomUUID().toString().substring(0, 8);
if (payment.charge(userId, amount)) {
notifier.notify(userId, "Order " + orderId + " placed for $" + amount);
return orderId;
}
throw new RuntimeException("Payment failed");
}
}
public static void main(String[] args) {
// Production wiring โ by hand (or by a DI container like Spring)
PaymentGateway gateway = new StripeGateway();
NotificationService email = new EmailNotifier();
OrderService orders = new OrderService(gateway, email);
String orderId = orders.placeOrder("alice", 99.99);
System.out.println("Order placed: " + orderId);
// Test wiring โ swap in fake implementations (no framework needed)
PaymentGateway fakeGateway = (userId, amount) -> {
System.out.println("[TEST] Fake charge: " + userId + " $" + amount);
return true;
};
NotificationService fakeNotifier = (userId, msg) ->
System.out.println("[TEST] Fake notify: " + msg);
OrderService testOrders = new OrderService(fakeGateway, fakeNotifier);
testOrders.placeOrder("bob", 49.50);
}
}Sources
- https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-introduction โ Spring DI overview
- https://www.pearson.com/en-us/subject-catalog/p/effective-java/P200000000138 โ Effective Java, Item 5