How Dependency Injection Improves Code Maintainability and Testability
Takahiro Iwasa
3 min read
Architecting
This note describes how to create loosely coupled code using Dependency Injection.
Example of Tight Coupling
The following example demonstrates an employee management feature written in TypeScript. For simplicity, this example assumes that mocks cannot be dynamically configured.

export class Salary { readonly employeeId: number;
constructor(employeeId: number) { this.employeeId = employeeId; }
calculate(): number { let salary = 0; // ... salary = 200000; return salary; }}
export class Employee { private employeeId: number; private name: string; private salary: Salary;
constructor(employeeId: number, name: string) { this.employeeId = employeeId; this.name = name; this.salary = new Salary(this.employeeId); }
// Send an email by Amazon SES. Message text depends on time. notify(): void { const hour = (new Date()).getHours(); let title = `Hi ${this.name}`; const body = `Current Salary: ${this.salary.calculate()}`;
if (6 <= hour && hour <= 9) { title = `Good morning ${this.name}`; } else if (10 <= hour && hour <= 18) { title = `How's it going, ${this.name}?`; } (new SES()).sendEmail({title: title, body: body}); }}The problems are:
- Tightly coupled to the Salary class:
- The line
this.salary = new Salary(this.employeeId);directly couples theEmployeeandSalaryclasses. - Testing
Employee#notifybecomes challenging because it depends on the actualSalary#calculatemethod, making it harder to simulate different salary calculations or handle edge cases during testing.
- The line
- Tightly coupled to the system clock:
- The line
const hour = (new Date()).getHours();couples theEmployeeclass with the system clock. - Conditional logic testing for specific times becomes difficult.
- The line
- Tightly coupled to AWS SES:
- The line
(new SES()).sendEmail(...)directly couplesEmployeewith the AWS SES service. - Testing the
notifymethod results in actual email sends, which may not be feasible in development.
- The line
Refactoring with Dependency Injection
Using Dependency Injection (DI), we can make the code more modular and testable.

export interface ISalary { readonly employeeId: number; calculate(): number;}
export interface ISystemDate { now(): Date;}
export interface IMailer { send(config: any): void;}
export class Salary implements ISalary { readonly employeeId: number;
constructor(employeeId: number) { this.employeeId = employeeId; }
calculate(): number { let salary = 0; // ... salary = 200000; return salary; }}
export class SystemDate implements ISystemDate { now(): Date { return new Date(); }}
export class EmployeeSes implements IMailer { send(config: any): void { (new SES()).sendEmail(config); }}
export class Employee { private employeeId: number; private name: string; private salary: ISalary;
constructor(employeeId: number, name: string, salary: ISalary) { this.employeeId = employeeId; this.name = name; this.salary = salary; }
// Send an email by Amazon SES. Message text depends on time. notify(systemDate: ISystemDate, mailer: IMailer): void { const hour = systemDate.now().getHours(); let title = `Hi ${this.name}`; const body = `Current Salary: ${this.salary.calculate()}`;
if (6 <= hour && hour <= 9) { title = `Good morning ${this.name}`; } else if (10 <= hour && hour <= 18) { title = `How's it going, ${this.name}?`; } mailer.send({title: title, body: body}); }}Key improvements:
- Reduced Coupling:
- Components like
Salary,SystemDate, andEmployeeSesare now injected via interfaces. - The
Employeeclass is no longer directly dependent on specific implementations.
- Components like
- Easier Testing:
- Mock implementations of
ISalary,ISystemDate, andIMailercan be used for testing. - System dependencies like clocks and SES services are decoupled.
- Mock implementations of
- Dependency Inversion Principle:
- High-level modules (
Employee) are independent of low-level modules (Salary,Date,SES).
- High-level modules (