Overview
Dependency injection (DI) means an object receives its collaborators from outside instead of creating them itself. OrderService takes a PaymentGateway in its constructor rather than calling new StripeGateway(). The code that wires objects together, the composition root, is the only place that knows concrete classes.
DI is how the Dependency Inversion Principle is applied in practice. It makes classes testable with fakes, keeps construction logic in one place, and lets you swap implementations by configuration. Constructor injection is preferred because dependencies are explicit and objects are fully initialized; containers like Spring, NestJS, Guice, and .NET DI automate wiring.
A chef does not grow vegetables or raise cattle; ingredients are delivered. The restaurant can switch suppliers without retraining the chef, and a cooking school can supply practice ingredients.
When to use it
- Any class with external collaborators (repositories, gateways, clocks, loggers).
- Unit testing business logic.
- Swapping implementations by environment or configuration.
Where it shows up in interviews
Recognize it when: 'how would you test this?'
- Design a notification service
- Design a payment processor
Recognize it when: main method assembling many classes.
- Design a parking lot
- Design an elevator system
Where it is used in real software
@Component, @Bean, and constructor injection are the backbone of Java enterprise apps.
Built-in DI containers inject services into components and controllers.
Go projects often wire dependencies explicitly in main or with tools like Wire.
Key terms
- Constructor injection
- Dependencies passed via the constructor.
- Setter / method injection
- Dependencies set after construction or per call.
- Composition root
- Single place where the object graph is built.
- DI container
- Framework that creates and wires objects.
- Scope / lifetime
- Singleton, per request, or transient instances.
How it works, step by step
- 1Identify dependencies
Anything created with new inside business classes.
- 2Depend on interfaces
PaymentGateway, not StripeGateway.
- 3Accept them in the constructor
Store as final/readonly fields.
- 4Wire at the composition root
main(), module setup, or container config.
- 5Inject fakes in tests
No infrastructure required.
Hard-wired vs injected
ReminderService sends emails at the right time
| Concern | new SmtpMailer() and new Date() inside | Injected Mailer and Clock |
|---|---|---|
| Unit test | Sends real emails, depends on real time | Fake mailer and fixed clock |
| Switch to SES | Edit ReminderService | Wire SesMailer in main |
| Hidden dependencies | Yes | Visible in constructor |
NOWConcern: Unit test | new SmtpMailer() and new Date() inside: Sends real emails, depends on real time | Injected Mailer and Clock: Fake mailer and fixed clock
DI makes dependencies explicit and replaceable.
Implementation
interface Mailer { send(to: string, subject: string): Promise<void> }interface Clock { now(): Date } class ReminderService { constructor(private mailer: Mailer, private clock: Clock) {} async remindIfDue(task: { owner: string; dueAt: Date }) { if (task.dueAt.getTime() - this.clock.now().getTime() <= 3_600_000) { await this.mailer.send(task.owner, "Task due within an hour"); return true; } return false; }} // Composition root (production)const service = new ReminderService(new SesMailer(), { now: () => new Date() }); // Testconst sent: string[] = [];const test = new ReminderService({ send: async (to) => { sent.push(to); } }, { now: () => new Date("2026-01-01T09:30:00Z") });await test.remindIfDue({ owner: "[email protected]", dueAt: new Date("2026-01-01T10:00:00Z") }); // sent = ["[email protected]"]Complexity and performance
Wiring happens once.
Framework dependent.
Trade-offs
Manual DI is explicit but verbose; containers reduce boilerplate but can hide wiring errors until runtime.
Many constructor parameters signal a class with too many responsibilities.
Variants and related techniques
Objects request dependencies from a registry; hides dependencies and is usually discouraged.
In functional code, pass dependencies as arguments or via closures.
Common mistakes
- Field injection (@Autowired on fields).
Fix: Prefer constructor injection for immutability and testability.
- Injecting the container itself.
Fix: That is a service locator; inject specific dependencies.
- Wrong scope (request-scoped bean in a singleton).
Fix: Understand lifetimes and use providers or factories.
Interview questions
Why prefer constructor injection?
Dependencies are explicit and required, fields can be final, objects are always fully initialized, and tests can pass fakes without reflection.
DI vs DIP?
DIP is the principle of depending on abstractions; DI is the technique of supplying implementations from the outside, typically through constructors.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Refactor a class that creates its own dependencies | Easy | Constructor injection. |
| Write a composition root for a parking lot | Medium | Wiring. |