OOP Concepts · Object Relationships

Dependency Injection Basics

Dependency injection (DI) is a simple idea with a fancy name: a class should receive the objects it depends on, not build them itself. That one change makes code easy to test and easy to change.

Intermediate 5 min Lesson 21 of 38 Runnable examplesExamples use modern JavaScript (ES2022 private fields) and C# 13.
0/38 in OOP Concepts
On this page · 5 min · Intermediate

In simple words#

A restaurant doesn’t grow its own vegetables; a supplier delivers them. Dependency injection means an object receives what it needs instead of creating it itself.

Dependency
Something a class needs to work.
Injection
Passing it in (usually via the constructor).
DI container
Creates and supplies dependencies automatically.

Learning objectives#

  • Explain what a dependency is
  • Inject dependencies through the constructor
  • Replace a real dependency with a fake in a test
  • Recognise DI containers like ASP.NET Core’s

Explanation#

A dependency is any object a class needs to do its job: an email sender, a database, a logger, a clock.

Without DI, the class creates its dependencies with new inside itself. It is then glued to that exact implementation — you cannot test it without really sending emails.

With DI, the dependency is passed in, usually through the constructor. The class only knows “something that can send(to, message)”.

In tests you pass a fake that records calls. In production you pass the real one. The class itself never changes.

Frameworks automate this with a DI container: in ASP.NET Core you register builder.Services.AddScoped<IEmailSender, SmtpEmailSender>() once, and every class that asks for IEmailSender in its constructor receives one.

Syntax#

C# Illustrative
public class SignupService(IEmailSender mailer)       // constructor injection
{
    public Task RegisterAsync(string email) => mailer.SendAsync(email, "Welcome!");
}


// Program.cs — tell the container what to inject
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<SignupService>();

Code example: A signup service, real and fake mailers#

JavaScriptA signup service, real and fake mailers Runnable
class SmtpEmailSender {
  send(to, message) { return `SMTP -> ${to}: ${message}`; }
}


class FakeEmailSender {
  sent = [];
  send(to, message) { this.sent.push({ to, message }); return 'stored'; }
}


// ✗ Without DI: the class builds its own dependency
class SignupServiceHardcoded {
  #mailer = new SmtpEmailSender();
  register(email) { return this.#mailer.send(email, 'Welcome!'); }
}


// ✓ With DI: the dependency is passed in
class SignupService {
  constructor(mailer) { this.mailer = mailer; }
  register(email) {
    if (!email.includes('@')) return 'invalid email';
    return this.mailer.send(email, 'Welcome!');
  }
}


console.log(new SignupServiceHardcoded().register('asha@example.com'));
console.log(new SignupService(new SmtpEmailSender()).register('ravi@example.com'));


// In a test: inject a fake and inspect what happened
const fake = new FakeEmailSender();
const service = new SignupService(fake);
service.register('test@example.com');
service.register('bad-email');
console.log(fake.sent);
Expected output
SMTP -> asha@example.com: Welcome!
SMTP -> ravi@example.com: Welcome!
[ { to: 'test@example.com', message: 'Welcome!' } ]
Opens this example in the full playground

Step-by-step explanation#

  1. SignupServiceHardcoded always uses SMTP — there is no way to test it without a mail server.
  2. SignupService receives its mailer in the constructor and only calls mailer.send().
  3. Production code passes the real SmtpEmailSender.
  4. The test passes a FakeEmailSender that just records messages, then checks that only the valid email was sent.

Real-world use case#

Every ASP.NET Core controller uses DI: public OrdersController(IOrderService orders, ILogger<OrdersController> logger). The framework builds and passes these objects for every request.

Common mistakes#

new-ing services inside classes

var repo = new SqlOrderRepository(); inside a controller makes it untestable and tied to SQL. Ask for IOrderRepository in the constructor.

Service locator

Calling a global container (ServiceLocator.Get<IMailer>()) inside methods hides dependencies. Constructor injection makes them visible.

Too many constructor parameters

Eight injected services means the class does too much. Split it (Single Responsibility Principle).

Best practices#

  • Prefer constructor injection
  • Depend on interfaces, not concrete classes
  • Create objects in one place (the composition root, e.g. Program.cs)
  • Keep the number of dependencies small

Remember this#

Don’t new it — receive it.

Real in production, fake in tests — same class.

DI container = automatic “pass the dependencies” machine.

Practice exercise#

Your task: Write a Greeter class that receives a clock object with a getHour() method. greet() returns "Good morning" before 12 and "Good afternoon" otherwise. Test it with two fake clocks.

Need a hint?
A fake clock can be a plain object: { getHour: () => 9 }.
Show solution
JavaScript Runnable
class Greeter {
  constructor(clock) { this.clock = clock; }
  greet() { return this.clock.getHour() < 12 ? 'Good morning' : 'Good afternoon'; }
}
console.log(new Greeter({ getHour: () => 9 }).greet());  // Good morning
console.log(new Greeter({ getHour: () => 15 }).greet()); // Good afternoon
Opens this example in the full playground

Mini quiz#

Dependency Injection Basics quizMultiple choice Question 1 of 3

What is constructor injection?

Score so far: 0/0

Want more? Practise all OOP Concepts MCQs · OOP Concepts interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.