Articles

What Is Dependency Injection?

Dependency injection passes an object's dependencies in from outside rather than letting it construct them, making code easier to test and swap.

The Lycoris Team The Lycoris Team · · 4 min read
Code editor showing modular application components

Dependency injection is a pattern where an object receives the other objects it depends on from the outside — usually as constructor or function arguments — instead of creating them itself internally. The object stops knowing how to build a database client, a logger, or an API wrapper; it just declares that it needs one, and something else hands it over. That single shift is behind most of what makes large codebases testable and swappable rather than a tangle of hardcoded dependencies.

The problem it solves

Without dependency injection, a class that needs a database connection typically constructs one directly:

class UserService {
  constructor() {
    this.db = new PostgresClient(process.env.DATABASE_URL);
  }
}

This looks harmless until you try to test UserService in isolation. Every test now needs a real Postgres connection, because the class builds its own client internally and there’s no way to intercept that. Swapping databases, or using a mock in tests, means editing UserService itself. The dependency is hidden inside the class rather than visible at its boundary.

The same code, inverted

Dependency injection just moves the construction outside the class and passes the result in:

class UserService {
  constructor(db) {
    this.db = db;
  }
}

const service = new UserService(new PostgresClient(process.env.DATABASE_URL));

UserService no longer knows or cares what db actually is — only that it satisfies whatever interface the class calls methods on. Testing becomes trivial: pass in a fake object with the same method names and no real database is ever touched.

const fakeDb = { query: () => Promise.resolve([{ id: 1, name: "test" }]) };
const service = new UserService(fakeDb);

This is the core mechanism — everything else (containers, decorators, framework magic) is tooling built on top of this one idea: dependencies come in through the front door, not built silently inside.

Constructor injection, method injection, and containers

Constructor injection — passing dependencies as constructor arguments, as above — is the most common form because it makes an object’s requirements visible at the single point where it’s created, and it guarantees the object never exists in a half-initialized state missing a dependency it needs.

Method injection passes a dependency into a single method call rather than the whole object’s lifetime, useful when only one operation needs it: service.notify(emailClient) rather than storing emailClient on the instance for its entire life.

Dependency injection containers (common in frameworks like NestJS, Spring, or Angular) automate the wiring: you register which concrete class satisfies which interface once, and the container resolves and constructs the full graph of dependencies for you at startup, based on constructor signatures or decorators. This removes the manual work of instantiating every object’s dependencies by hand as an application grows, at the cost of some indirection — reading a decorated class doesn’t always show you, at a glance, what gets injected at runtime.

Why this matters for testing specifically

The most concrete payoff is unit testing without a network, a database, or a filesystem. A class that receives its collaborators from outside can be tested with fakes or mocks that return canned responses instantly, rather than exercising a full stack for every test. This is the same reasoning behind interfaces vs concrete types in TypeScript — depending on an interface rather than a specific implementation is what makes swapping the real thing for a fake possible in the first place, whether or not a formal DI container is involved.

It also has an architectural side effect: a class whose dependencies are all passed in rather than constructed internally can’t reach out and create side effects on its own. That constraint tends to push logic toward smaller, more composable pieces, which is part of why dependency injection shows up so often alongside other structural patterns for keeping services loosely coupled, like the ones described in the twelve-factor app methodology.

Dependency injection vs a service locator

A related but distinct pattern is the service locator, where instead of receiving dependencies directly, an object asks a global registry for what it needs at runtime: const db = ServiceLocator.get("db"). This solves the same construction problem but keeps the dependency hidden inside the method body rather than declared at the boundary — you can’t tell what a class needs just by looking at its constructor signature, and tests still need to configure the global locator rather than simply passing in a fake. Dependency injection is generally preferred for this reason: the requirements are explicit and local rather than implicit and global.

Where this shows up beyond a single class

The pattern scales past individual objects into how whole services are composed. In a monolith, a DI container might wire together the entire application’s object graph at boot. In a service split into smaller pieces, the same idea applies at a coarser grain: a service depends on an abstract interface to another service (an ORM sitting between application code and the database is a common example), and the concrete implementation is swapped in through configuration rather than hardcoded — the same win, just at a bigger scale. Whether it’s a single class or a whole monorepo full of services, the underlying question is the same: does this piece of code know how to build its dependencies, or does it only know how to use them?

The takeaway

Dependency injection moves the construction of an object’s dependencies outside that object, replacing hidden internal new SomeClient() calls with dependencies passed in from the caller. The direct payoff is testability — fakes and mocks slot in wherever a real dependency used to be constructed — and the broader one is decoupling: code that only knows the shape of what it depends on, not how to build it, is easier to swap, extend, and reason about as a system grows past a size where any one person holds the whole thing in their head.

The Lycoris Team The Lycoris Team · · 4 min read

How Regular Expressions Work Under the Hood

Regular expressions are matched by finite automata or backtracking engines. How regex engines parse patterns, and why some patterns run slowly.

#Computer Science #Algorithms #Developer Tools
The Lycoris Team The Lycoris Team · · 5 min read

What Is Little's Law? Capacity Planning Explained

Little's Law relates the number of requests in a system, their arrival rate, and how long each one takes — a simple formula for sizing capacity.

#Computer Science #Performance #Backend
The Lycoris Team The Lycoris Team · · 4 min read

API Versioning Strategies Explained

URI paths, custom headers, and content negotiation are the three common ways to version an API. Tradeoffs of each, and how to avoid breaking clients.

#Web Development #Developer Tools #Backend