Dependency Injection
IntermediateA technique where an object receives its dependencies from an external source rather than creating them itself, enabling loose coupling and testability.
Overview
Dependency Injection (DI) is the application of the Dependency Inversion Principle. Instead of a class calling new on its dependencies, the dependencies are "injected" — pushed in from outside. There are three injection types: Constructor (recommended — dependencies immutable after construction, easy to test), Setter (optional dependencies), and Field (@Autowired — convenient but hides dependencies, hard to test without framework). DI containers (Spring, Guice, Dagger) manage object lifecycles and wiring automatically. Manual DI (Pure DI) is viable for small codebases and eliminates framework dependency.
Constructor vs Setter vs Field Injection
Constructor injection is the only type that guarantees a fully initialized object and works without a DI framework. Spring recommends constructor injection since version 4.3, which even omits @Autowired when there is a single constructor.
// ❌ Anti-pattern: creating dependencies internally (tight coupling)
public class OrderService {
private final PaymentGateway gateway = new StripePaymentGateway(); // hard dependency
private final EmailClient emailer = new SendGridEmailClient();
// Cannot swap StripePaymentGateway in tests — unit testing is impossible
}
// ✅ Constructor Injection (preferred)
public class OrderService {
private final PaymentGateway gateway;
private final NotificationService notifier;
// Spring auto-detects single constructor — @Autowired optional in Spring 4.3+
public OrderService(PaymentGateway gateway, NotificationService notifier) {
this.gateway = Objects.requireNonNull(gateway, "gateway required");
this.notifier = Objects.requireNonNull(notifier, "notifier required");
}
public Order placeOrder(Cart cart) {
Order order = Order.from(cart);
gateway.charge(order.getTotalAmount()); // depends on interface, not impl
notifier.sendConfirmation(order);
return order;
}
}
// ✅ Setter Injection — for optional dependencies
public class ReportService {
private final DataSource dataSource; // required
private Logger logger; // optional — has a default
public ReportService(DataSource dataSource) { this.dataSource = dataSource; }
@Autowired(required = false)
public void setLogger(Logger logger) { this.logger = logger; }
}
// ❌ Field Injection — avoid in production code
@Service
public class UserService {
@Autowired private UserRepository repo; // hidden dependency, not testable without Spring
}
// Spring wiring (Java config)
@Configuration
public class AppConfig {
@Bean
public PaymentGateway paymentGateway() { return new StripePaymentGateway(); }
@Bean
public OrderService orderService(PaymentGateway gw, NotificationService ns) {
return new OrderService(gw, ns);
}
}Manual DI (Pure DI) & Testing
Pure DI wires the object graph in main() or a dedicated composition root without a framework. For unit tests, inject test doubles (mocks/stubs) via constructor injection — no Spring context needed.
// Interfaces
public interface PaymentGateway { void charge(BigDecimal amount); }
public interface NotificationService { void sendConfirmation(Order order); }
// Test double — stub for unit tests
public class FakePaymentGateway implements PaymentGateway {
private final List<BigDecimal> charges = new ArrayList<>();
@Override
public void charge(BigDecimal amount) { charges.add(amount); }
public List<BigDecimal> getCharges() { return Collections.unmodifiableList(charges); }
}
// Unit test — zero Spring context, pure Java
class OrderServiceTest {
@Test
void placeOrder_chargesCorrectAmount() {
FakePaymentGateway fakeGateway = new FakePaymentGateway();
NotificationService fakeNotifier = order -> {}; // lambda stub
OrderService sut = new OrderService(fakeGateway, fakeNotifier);
Cart cart = Cart.withItems(List.of(new Item("LLD Book", new BigDecimal("499.00"))));
sut.placeOrder(cart);
assertEquals(1, fakeGateway.getCharges().size());
assertEquals(new BigDecimal("499.00"), fakeGateway.getCharges().get(0));
}
}Key Points to Remember
- 1Constructor injection is preferred — produces immutable, fully initialized objects without framework dependency.
- 2Field injection (@Autowired on fields) hides dependencies and requires a Spring context for unit tests.
- 3DI enables swapping implementations — production uses Stripe, tests use FakePaymentGateway.
- 4Spring's IoC container manages bean lifecycle: creation, wiring, initialization, and destruction.
- 5Circular dependencies (A needs B, B needs A) signal a design smell — break the cycle by extracting a third class.
- 6@Lazy can break circular constructor dependencies as a last resort, but prefer refactoring.
Interview Questions
Sign in to ask AriaWhat are the three types of dependency injection? Which do you prefer and why?
Why is constructor injection preferred over field injection?
How would you handle a circular dependency in Spring?
What is the difference between DI and the Service Locator pattern?
How does dependency injection make code more testable?
Ask Aria about Dependency Injection
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.