awesome-everything RU
↑ Back to the climb

Backend Architecture

Inversion of control: how dependencies reach a class

Crux A class that constructs its own collaborators is welded to them. Inversion of control hands a class what it needs from the outside — and the choice between constructor injection, a service locator, and in-class `new` decides how testable and honest your code is.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at middle altitude — in the sky
◷ 14 min

A team wants to test their OrderService. The test creates one — and a real Postgres connection opens, a real Stripe call fires, and a real email is sent to a real address. They cannot test the logic without the entire production stack, because the constructor does this.db = new PgClient(), this.payments = new StripeClient(), this.mail = new Mailer(). The logic was never the problem. The wiring was.

The coupling created by new

When a class writes new EmailService() inside itself, it has made an irreversible decision for every caller: this code is the real email service, always. You cannot substitute a fake in a test, swap a different implementation per environment, or reuse the class with a different collaborator. The dependency is hidden (invisible in the type) and hard (welded at construction).

Inversion of control

Inversion of control (IoC) flips who decides. Instead of the class choosing its concrete collaborator, the class declares an abstract need and something else supplies the concrete one. Dependency injection is the most common form of IoC: dependencies are injected from outside rather than constructed inside.

The cleanest injection is constructor injection:

class OrderService {
  constructor(
    private db: Database,
    private payments: PaymentGateway,
    private mail: Mailer,
  ) {}
}

Now the constructor signature is an honest, compile-time-checked list of everything the class needs. A test passes in fakes; production passes in real ones. The dependency became visible and soft.

Service locator: injection’s tempting cousin

A service locator also avoids new, but the class asks a global registry for its dependencies: const db = Container.get('Database'). This looks like DI but reintroduces the original sin — the dependencies are hidden again (the constructor no longer lists them), and the class is coupled to the locator. It is widely considered an anti-pattern in application code precisely because it hides what a class needs and only fails at runtime, not at compile time or startup.

Why this works

Why is service locator an anti-pattern if it also removes the new? Both remove the hard-coded construction, but they differ on honesty. With constructor injection, the dependencies are right there in the signature — the compiler and the reader both see them, and a missing one fails at startup. With a service locator, the constructor looks empty while the class secretly reaches into a global container at runtime; a missing or misconfigured dependency surfaces as a runtime crash deep in a code path, and tests must configure the global locator instead of just passing arguments. The locator is legitimate in one place — inside the DI framework or composition root itself — but not scattered through business logic.

The composition root

If classes no longer construct their dependencies, something must. That something is the composition root: a single place near the application’s entry point where the entire object graph is assembled — the only place that names concrete implementations. Everything downstream depends only on abstractions. Centralizing construction means there is exactly one spot to change wiring, swap implementations, or read off the whole dependency graph. A DI container automates this assembly, but the principle holds even with hand-wiring: keep construction at the edge, keep logic in the middle dependency-free.

ApproachDependencies areFails whenTestable
new inside classHidden + hard-wiredNever swappableNo
Service locatorHidden, fetched at runtimeAt runtime, deep in a pathAwkward
Constructor injectionVisible in signatureAt startup / compileYes
Quiz

Why is `new PgClient()` inside a service constructor a testability problem?

Quiz

A class has an empty constructor but calls `Container.get('Mailer')` internally whenever it needs to send mail. What is the main objection?

Order the steps

Order the steps of refactoring a welded class to constructor injection:

  1. 1 Identify the `new` calls hidden inside the class
  2. 2 Define abstractions (interfaces/types) for those dependencies
  3. 3 Add them as constructor parameters typed by the abstractions
  4. 4 Move the concrete construction to the composition root
  5. 5 In tests, pass fakes through the constructor instead of real services
Recall before you leave
  1. 01
    What two properties make in-class `new` calls a coupling problem, and how does constructor injection fix them?
  2. 02
    Why is a service locator considered an anti-pattern even though it removes the hard-coded `new`?
  3. 03
    What is the composition root and why centralize construction there?
Recap

Constructing collaborators with new inside a class welds it to concrete implementations and hides them in the process, which is why a service built that way drags the whole production stack into every test. Inversion of control flips the decision: the class declares abstract needs and receives concrete instances from outside. Constructor injection is the honest form — dependencies are listed in the signature, visible to compiler and reader, and a missing one fails fast at startup. A service locator removes the new but re-hides dependencies behind a global registry and defers failure to runtime, which is why it is an anti-pattern outside framework internals. All of this requires a composition root: one edge where the real object graph is assembled, leaving the logic in the middle dependency-free. The next question is the lifetime of those wired objects — singletons, request scope, transient — and the subtle bugs each scope invites.

Connected lessons
appears again in185
Continue the climb ↑DI scopes and lifecycles: singleton, request, transient
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.