
π§ What is Dependency Injection?
Dependency Injection (DI) is a design pattern where a class receives its dependencies from the outside instead of creating them internally. Instead of a service instantiating the concrete classes it needs, it receives an interface (abstraction) through its constructor.
This simple technique has a massive impact on how testable, flexible, and maintainable your code becomes.
π« The Problem: Tight Coupling
Imagine a UserService that needs to fetch users from a database. Without DI, it directly creates its own repository:
class UserRepository {
findById(id: string): User {
// Hits the real database
return database.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
class UserService {
private userRepo = new UserRepository(); // β Hardcoded dependency
getUser(id: string): User {
return this.userRepo.findById(id);
}
}
What's wrong here?
UserServiceis tightly coupled toUserRepository- You cannot test
UserServicewithout a real database connection - If you want to swap the data source (e.g., from SQL to an API), you have to modify
UserService
β The Solution: Depend on Interfaces, not Implementations
The key idea is:
- Define an interface for the dependency
- Have the service depend on the interface, not the concrete class
- Inject the concrete implementation through the constructor
/** Step 1: Define the interface **/
interface IUserRepository {
findById(id: string): User;
}
/** Step 2: Service depends on the interface **/
class UserService {
constructor(private userRepo: IUserRepository) {} // π‘ Parameter properties
getUser(id: string): User {
return this.userRepo.findById(id);
}
}
/** Step 3: Create the real implementation **/
class UserRepository implements IUserRepository {
findById(id: string): User {
return database.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
Now, UserService doesn't know or care which implementation it receives. It only knows about IUserRepository.
/** Usage in production **/
const userRepo = new UserRepository();
const userService = new UserService(userRepo);
π§ͺ Why It Matters: Testing
This is where DI truly shines. Since UserService depends on an interface, we can create a mock implementation for testing, no database required:
/** Mock implementation for tests **/
class UserRepositoryMock implements IUserRepository {
findById(id: string): User {
return { id, name: "Test User", email: "test@example.com" };
}
}
/** Unit test (no database needed!) **/
const mockRepo = new UserRepositoryMock();
const userService = new UserService(mockRepo);
const user = userService.getUser("123");
assert(user.name === "Test User"); // β
Fast, isolated, reliable
π« Without DI: Your tests need a real database, making them slow, and hard to set up.
β
With DI: Tests are fast, isolated, and fully under your control.
π Before vs After: A Visual Summary
Without DI (tight coupling):
UserService βββββββΊ UserRepository βββββββΊ Database
(cannot swap) (concrete)
With DI (loose coupling):
UserService βββββββΊ IUserRepository (interface)
β
ββββββ΄ββββββ
βΌ βΌ
UserRepository UserRepositoryMock
(production) (testing)
The interface acts as a contract: any class that implements it can be plugged into UserService. This makes your code flexible and your tests reliable.
π‘ Key Takeaways
- Depend on abstractions (interfaces), not concrete implementations
- Inject dependencies through the constructor instead of creating them internally
- This makes your classes easy to test by swapping real implementations with mocks
- It also makes your code easier to change (swap a SQL repository for an API client without touching the service)
If you're familiar with SOLID principles, you'll notice that Dependency Injection is the practical application of the Dependency Inversion Principle (DIP): "High-level modules should not depend on low-level modules. Both should depend on abstractions."
Conclusion
Dependency Injection is one of those patterns that, once you understand it, you'll want to apply everywhere. It keeps your code decoupled, your tests fast and reliable, and your architecture flexible.
Next time you write new SomeDependency() inside a class, stop and ask yourself: should I inject this instead? π―