· software-engineers Editorial · Career  · 6 min read

Dependency Injection Patterns Practical Guide

Constructor, setter, and interface injection compared, plus DI container tradeoffs across Spring, NestJS, and manual wiring in 2026.

What Dependency Injection Actually Solves

Dependency injection (DI) is not a framework feature — it’s a technique for inverting control over object creation so that a class receives its collaborators from the outside rather than constructing them itself. The payoff: classes become testable in isolation, swappable at runtime, and decoupled from concrete implementations.

Without DI, a class like OrderService might do new PostgresOrderRepository() inside its constructor. That hardcodes a dependency, making the service impossible to unit test without a real database, and impossible to swap for a different implementation without editing the class itself. DI flips this: OrderService receives an OrderRepository (interface) via constructor argument, and something else — a DI container, a factory, or a manually written composition root — decides which concrete implementation to hand over.

This single inversion is why DI shows up constantly in both take-home assignments and live coding interviews in 2026: it’s the fastest way to demonstrate you understand testability and coupling without writing a 10-page design doc.

The Three Core Injection Patterns

Constructor injection: dependencies passed as constructor arguments. This is the dominant pattern in 2026 across TypeScript (NestJS), Java (Spring), and C# (ASP.NET Core) codebases because it makes dependencies explicit and required — you cannot instantiate the object in an invalid state, and dependencies are immutable (readonly/final) after construction.

class OrderService {
  constructor(
    private readonly repository: OrderRepository,
    private readonly notifier: NotificationPort
  ) {}
}

Setter/property injection: dependencies assigned via public setters or properties after construction. Useful for optional dependencies or circular dependency resolution, but it allows an object to exist in a partially-configured, invalid state before setters are called — a common source of null-reference bugs.

Interface injection: the dependency itself defines an injector method that the receiving class implements, and the injecting framework calls it. Rare in modern codebases outside older Java EE-style frameworks; mostly of historical/interview-trivia interest today.

Method injection: dependencies passed as parameters to individual methods rather than stored as fields at all — appropriate when a dependency is needed for exactly one operation and shouldn’t be part of the object’s persistent state (common in functional-leaning codebases).

DI Containers vs. Manual Wiring

A DI container (Spring’s ApplicationContext, NestJS’s module system, .NET’s IServiceCollection) automates the wiring: you register interfaces to implementations once, and the container resolves the full dependency graph, including transitive dependencies, at object creation time.

Manual wiring means writing the composition root by hand — literally calling constructors in the right order in one main.ts or bootstrap.ts file. This sounds primitive but many teams in 2026 (especially in Go, and increasingly in TypeScript projects influenced by hexagonal architecture advocates) explicitly prefer it: no reflection magic, no runtime surprises from misconfigured bindings, and the entire object graph is visible in one readable file.

The tradeoff is graph size. Manual wiring for 5-10 services is trivially readable. Past 50-100 services with complex lifetimes (singleton, request-scoped, transient), hand-wiring becomes its own maintenance burden, and a container’s declarative registration wins.

Comparison: DI Approaches Across Ecosystems

ApproachExplicitnessRuntime overheadCompile-time safetyTypical scale fit
Constructor injection, manual wiringHighest — full graph in one fileNoneFull (TS/Java compiler catches mismatches)Small-to-mid codebases, <50 services
Constructor injection, DI container (Spring, NestJS)Medium — registrations scattered across modulesLow-moderate (reflection/decorators at startup)Partial (misconfigured bindings often fail at runtime, not compile time)Mid-to-large codebases, microservices
Setter injectionLow — object can exist unconfiguredNoneWeak — no guarantee setter was calledLegacy codebases, optional dependencies only
Service locator (anti-pattern)Lowest — dependencies hidden inside method bodiesLowNone — failures surface only at call timeAvoid; common in interviews as a “spot the anti-pattern” question

The Service Locator Anti-Pattern (and Why Interviewers Test for It)

A service locator is a global registry that classes query at runtime (ServiceLocator.get(OrderRepository)) instead of receiving dependencies through their constructor. It looks like DI but isn’t — it hides dependencies inside method bodies instead of declaring them in the public constructor signature, making it impossible to tell what a class needs just by reading its interface. Unit tests must set up global registry state before every test, creating hidden coupling between test files.

Interviewers routinely present a service-locator-based snippet and ask candidates to spot what’s wrong. The tell: any place code calls a static/global .get() or .resolve() method mid-method rather than receiving the value as a parameter. The 0-to-1 SWE Interview Playbook (https://www.amazon.com/dp/B0H256Z1MF?tag=sirjohnnymai-20) includes a dedicated “spot the anti-pattern” drill set covering service locator, along with singleton overuse and circular dependency traps.

Handling Circular Dependencies

Circular dependencies (Service A needs Service B, Service B needs Service A) are a design smell more often than a DI limitation — they usually indicate the two services should be merged, or a shared responsibility should be extracted into a third service. When genuinely unavoidable (rare, but happens with bidirectional event listeners), setter injection or a lazy Provider<T>/factory wrapper breaks the cycle by deferring resolution until first use rather than at construction time.

Testing With DI: The Real Payoff

The entire reason DI earns its complexity budget is test isolation:

const fakeRepo: OrderRepository = { findById: jest.fn(), save: jest.fn() };
const fakeNotifier: NotificationPort = { send: jest.fn() };
const service = new OrderService(fakeRepo, fakeNotifier);

No test framework magic, no database spin-up, no network calls — because the class only knows about interfaces, substituting fakes is a constructor call. This is why DI and hexagonal/ports-and-adapters architecture are frequently taught together: DI is the mechanism, ports-and-adapters is the architectural philosophy that decides where the interface boundaries go.

FAQ

Q: Is a DI container required to “do” dependency injection? A: No. DI is a technique (pass dependencies in rather than construct them internally); a container is an optional tool that automates the wiring for large graphs. Small services benefit more from manual wiring’s transparency than from container magic.

Q: Why do interviewers prefer constructor injection over setter injection? A: Constructor injection makes required dependencies impossible to omit — the object cannot be instantiated in an invalid, half-configured state. Setter injection allows exactly that, which is a common source of null-pointer/undefined bugs in production.

Q: How is dependency injection different from the dependency inversion principle? A: Dependency inversion is the design principle: high-level modules should depend on abstractions, not concrete low-level modules. Dependency injection is one concrete technique for achieving that principle in code — the constructor/setter mechanics of handing over the abstraction’s implementation at runtime.

Understanding this distinction — principle versus mechanism — is exactly the kind of precision that separates a strong system design answer from a mediocre one in 2026 interview loops.

Back to Blog

Related Posts

View All Posts »