Home/Learn/Low Level Design/Dependency Injection

Dependency Injection

Intermediate
Creational Patterns

A 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.

Java — Constructor, Setter, Field injection comparison
// ❌ 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.

Java — Pure DI and unit testing with constructor injection
// 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 Aria
1

What are the three types of dependency injection? Which do you prefer and why?

EasyAmazon
2

Why is constructor injection preferred over field injection?

MediumGoogle
3

How would you handle a circular dependency in Spring?

HardAtlassian
4

What is the difference between DI and the Service Locator pattern?

MediumMicrosoft
5

How does dependency injection make code more testable?

EasyFlipkart

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.

Loading discussion…