CREATIONAL PATTERNS / OBJECT DESIGN BRIEF

Dependency injection

Dependency injection (DI) means an object receives its collaborators from outside instead of creating them itself.

BeginnerPhase 04 / Topic 6 of 6ResponsibilitiesCollaborationsExtensibility
01

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 and suppliers

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.

02

When to use it

  • Any class with external collaborators (repositories, gateways, clocks, loggers).
  • Unit testing business logic.
  • Swapping implementations by environment or configuration.
03

Where it shows up in interviews

Testable design

Recognize it when: 'how would you test this?'

  • Design a notification service
  • Design a payment processor
Wiring an LLD solution

Recognize it when: main method assembling many classes.

  • Design a parking lot
  • Design an elevator system
04

Where it is used in real software

Spring and Spring Boot

@Component, @Bean, and constructor injection are the backbone of Java enterprise apps.

Angular and NestJS

Built-in DI containers inject services into components and controllers.

Go and manual DI

Go projects often wire dependencies explicitly in main or with tools like Wire.

05

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

How it works, step by step

  1. 1
    Identify dependencies

    Anything created with new inside business classes.

  2. 2
    Depend on interfaces

    PaymentGateway, not StripeGateway.

  3. 3
    Accept them in the constructor

    Store as final/readonly fields.

  4. 4
    Wire at the composition root

    main(), module setup, or container config.

  5. 5
    Inject fakes in tests

    No infrastructure required.

07

Hard-wired vs injected

ReminderService sends emails at the right time

Step 1 / 3
Concernnew SmtpMailer() and new Date() insideInjected Mailer and Clock
Unit testSends real emails, depends on real timeFake mailer and fixed clock
Switch to SESEdit ReminderServiceWire SesMailer in main
Hidden dependenciesYesVisible 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.

08

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]"]
09

Complexity and performance

Runtime costNear zero

Wiring happens once.

Container startupMilliseconds to seconds

Framework dependent.

10

Trade-offs

Explicitness vs magic

Manual DI is explicit but verbose; containers reduce boilerplate but can hide wiring errors until runtime.

Constructor bloat

Many constructor parameters signal a class with too many responsibilities.

11

Variants and related techniques

Service locator

Objects request dependencies from a registry; hides dependencies and is usually discouraged.

Function parameters

In functional code, pass dependencies as arguments or via closures.

12

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.

13

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.

14

Practice problems

ProblemDifficultyWhat it trains
Refactor a class that creates its own dependenciesEasyConstructor injection.
Write a composition root for a parking lotMediumWiring.