Topic 3.5
Dependency Injection
In one line
The binding that holds LLD designs together: interface → implementation → constructor injection → Spring bean. It is the mechanism that makes strategy, port/adapter, and testability real.
Think of it like this
A torch that takes batteries. The torch doesn't make its own battery; you put one in. You can use a normal battery, a rechargeable one, or a test battery, and the torch doesn't care.
Key ideas
- 01
Constructor injection is the default: dependencies arrive as constructor params, fields are final.
- 02
Interface + implementation pattern (ports & adapters): domain depends on the port; the adapter (Postgres, Redis, HTTP) implements it.
- 03
Without DI, testing a service means mocking static calls; with DI you inject a fake implementation.
- 04
Spring resolves the wiring at startup: @Component on implementations, constructor injection everywhere else.
- 05
DIP is not the same as DI: DIP is the principle (depend on abstractions); DI is the mechanism (inject the concrete behind the abstraction).
Java / Spring map
- →
Spring: @Service CheckoutService(PaymentGateway gateway) — the container hands it the bean.
Code & diagrams
The full port/adapter chain the syllabus calls 'interface → implementation → constructor injection → Spring Bean'.
// 1. DOMAIN PORT (interface — the abstraction the domain depends on)
public interface TicketRepository {
Optional<Ticket> findById(String id);
Ticket save(Ticket t);
}
// 2. ADAPTER IMPLEMENTATION
@Repository
public class JpaTicketRepository implements TicketRepository {
private final TicketJpaRepository jpa;
public JpaTicketRepository(TicketJpaRepository jpa) { this.jpa = jpa; } // framework's own DI
public Optional<Ticket> findById(String id) { return jpa.findById(id); }
public Ticket save(Ticket t) { return jpa.save(t); }
}
// 3. CONSTRUCTOR INJECTION INTO THE SERVICE
@Service
public class ParkingService {
private final TicketRepository tickets; // depends on the PORT
public ParkingService(TicketRepository tickets) { this.tickets = tickets; } // constructor injection
public Ticket assign(String plate) {
Ticket t = new Ticket(plate, System.currentTimeMillis());
return tickets.save(t); // can't tell which adapter runs → testable
}
}
// TEST — swap the adapter:
// ParkingService svc = new ParkingService(new InMemoryTicketRepository());
// No Spring, no database, no mocks. The design earns its keep in the test.Explain without notes
What breaks in tests if TicketRepository is a concrete class with static methods?
Practice
Convert a service with new JdbcThing() inside to constructor-injected port/adapter.
Trade-offs
- ↔
Framework magic obscures wiring errors; constructor injection keeps it visible and compile-safe.
Completion checklist
I can draw interface → impl → constructor → Spring bean and say why each hop exists.