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.
On this page · 5 min · Intermediate
On this page
- In simple words
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 5 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
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#
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#
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);
SMTP -> asha@example.com: Welcome!
SMTP -> ravi@example.com: Welcome!
[ { to: 'test@example.com', message: 'Welcome!' } ]Step-by-step explanation#
- SignupServiceHardcoded always uses SMTP — there is no way to test it without a mail server.
- SignupService receives its mailer in the constructor and only calls mailer.send().
- Production code passes the real SmtpEmailSender.
- 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#
var repo = new SqlOrderRepository(); inside a controller makes it untestable and tied to SQL. Ask for IOrderRepository in the constructor.
Calling a global container (ServiceLocator.Get<IMailer>()) inside methods hides dependencies. Constructor injection makes them visible.
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?
Show solution
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
Mini quiz#
What is constructor injection?
Want more? Practise all OOP Concepts MCQs · OOP Concepts interview questions
My notes#
Saved in this browser.