Skip to content
Innovialabs

Dependency Injection Explained: Why and How to Use It

Advanced3 min

In short

Dependency injection means a class does not create its dependencies itself but receives them from outside, usually through the constructor. This makes code easier to test and change, and a DI container simply automates assembling objects.

The idea in one minute

Dependency injection (DI) is a technique where an object receives the dependencies it needs from outside instead of creating them internally. Most often through the constructor.

What it gives you:

  • Loose coupling. A class depends on an interface, not a concrete implementation.
  • Testability. In a test you can pass a stub instead of a real database or mail service.
  • Flexibility. Swapping an implementation means changing one assembly point.

DI is a specific case of the broader dependency inversion principle: high-level modules should not depend on low-level details; both depend on abstractions.

What tightly coupled code looks like

class OrderService {
  private db = new PostgresOrderRepository();
  private mailer = new SmtpMailer();

  async place(order: Order) {
    await this.db.save(order);
    await this.mailer.send(order.email, "Order received");
  }
}

Problems:

  • You cannot test place without running a database and an SMTP server.
  • Replacing the mail service means editing OrderService.
  • Dependencies are hidden: the class signature does not show what it needs.

Refactoring: constructor injection

interface OrderRepository { save(order: Order): Promise<void>; }
interface Mailer { send(to: string, text: string): Promise<void>; }

class OrderService {
  constructor(
    private readonly repo: OrderRepository,
    private readonly mailer: Mailer,
  ) {}

  async place(order: Order) {
    await this.repo.save(order);
    await this.mailer.send(order.email, "Order received");
  }
}

// Composition root
const service = new OrderService(new PostgresOrderRepository(), new SmtpMailer());

Now OrderService knows nothing about Postgres or SMTP. All concrete classes are created in one place, the composition root, usually at application startup.

How DI makes testing easier

test("saves the order and sends an email", async () => {
  const saved: Order[] = [];
  const sent: string[] = [];
  const service = new OrderService(
    { save: async o => { saved.push(o); } },
    { send: async to => { sent.push(to); } },
  );

  await service.place({ id: "1", email: "a@example.com" } as Order);

  expect(saved).toHaveLength(1);
  expect(sent).toEqual(["a@example.com"]);
});

No database, no network: the test is fast and stable.

DI containers

With dozens of classes, wiring them by hand gets tedious. A DI container is a library that stores "interface to implementation" rules, builds objects with the right dependencies and manages their lifetime.

ASP.NET Core has a built-in container:

builder.Services.AddScoped<IOrderRepository, PostgresOrderRepository>();
builder.Services.AddSingleton<IMailer, SmtpMailer>();
builder.Services.AddScoped<OrderService>();

public class OrderService
{
    private readonly IOrderRepository _repo;
    private readonly IMailer _mailer;

    public OrderService(IOrderRepository repo, IMailer mailer)
    {
        _repo = repo;
        _mailer = mailer;
    }
}

The main lifetimes in .NET:

LifetimeWhen an instance is created
TransientA new instance every time the dependency is requested
ScopedOne instance per scope; in a web app, per HTTP request
SingletonOne instance for the whole application

In the TypeScript world, containers come with frameworks (for example NestJS and Angular) and as standalone libraries. In a small project, manual wiring in a composition root is often enough.

Common mistakes

  • Service Locator instead of DI: the class pulls dependencies from the container itself, so they are hidden again.
  • A constructor with eight parameters signals that the class does too much.
  • A scoped dependency inside a singleton lives longer than it should.
  • An interface for every class without need; introduce abstractions where substitution is actually required.

FAQ

No. DI is the principle of passing dependencies from outside. A container only automates the wiring, and manual wiring is fine for small projects.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.