Command Palette

Search for a command to run...

PHASE 2Beginner ~15 min· topic 24 of 28Enterprise

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.

0/28 · 0%

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.

Checkout.javawhole filejava
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).

Letting Spring do the wiringdiagram
Rendering diagram…

Try it yourself

  1. 1

    Test the service with fakes

    No network or database needed: the test chooses the dependencies.

    terminal
    $ jshell> /open Checkout.java
    jshell> 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 + @Bean for 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().

terminal
$ ./mvnw -q test -Dtest=CheckoutServiceTest
── what you'll see ──
[ERROR] checkoutMarksOrderPaid Time elapsed: 0.01 s <<< ERROR!
java.lang.NullPointerException: Cannot invoke "in.tiffin.pay.PaymentGateway.charge(String, long)" because "this.payments" is null

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

    Without DI, a class calls new PaymentGateway() or Gateway.getInstance() inside itself. It's tied to that exact implementation, and a test can't replace it.

  2. 2

    With constructor injection, the class declares what it needs as constructor parameters (preferably interfaces). Whoever creates it (a test, a main method, or a framework) chooses the implementations.

  3. 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. 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. 5

    Prefer constructor injection over field injection (@Autowired on fields): dependencies are explicit, objects can be immutable, and tests can construct them without the container.

Explain it without notes

01

Why does constructor injection make code easier to test?

02

What is a composition root?

Practice

01

Refactor a class that calls new SmsClient() inside a method so it can be unit-tested.

02

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.

Back to phase