DEV Community

Thy Pham
Thy Pham

Posted on • Edited on

2 1

Dependency Inversion in practice (example code in typescript)

Summary

  • Dependency Inversion is a technique (as the name suggests) to invert the dependency from one module to another.
  • Without DI: A -> B (A depends on B)
  • With DI: (A -> I) <- B (B depends on A through interface I)
  • We can use DI to protect our core business logic module (and its tests) from depending on other modules (Database, HTTP API, etc...)

What is Dependency Inversion (DI)?

Imagine a class called UserService that needs to interact with MongoDB to save new user data. UserService will call createUser() function provided by UserRepoMongoDB class to serve this purpose. That means UserService depends on UserRepoMongoDB.

// file ./UserRepoMongoDB.ts
interface MongoDBUserSchema {
  // MongoDB user schema fields
}

class UserRepoMongoDB {
  createUser(name: string, age: number): Promise<MongoDBUserSchema> {
    // Some MongoDB specific logic to store user data in MongoDB
  }
}

export { UserRepoMongoDB };
Enter fullscreen mode Exit fullscreen mode
// file ./UserService.ts
import { UserRepoMongoDB } from "./UserRepoMongoDB";

class UserService {
  createUser() {
    return new UserRepoMongoDB(). createUser("Max Mustermann", 20);
  }
}

export { UserService };
Enter fullscreen mode Exit fullscreen mode

To make UserRepoMongoDB depends on UserService, we can create an interface IUserRepo with the createUser() function. UserService will use this function from IUserRepo instead. And UserRepoMongoDB will implement this interface:

// file ./IUserRepo.ts
interface User {
  name: string;
  age: string;
}

interface IUserRepo {
  createUser(name: string, age: number): Promise<User>;
}

export { User, IUserRepo };
Enter fullscreen mode Exit fullscreen mode
// file ./UserRepoMongoDB.ts
import { IUserRepo, User } from "./IUserRepo";

interface MongoDBUserSchema {
  // MongoDB user schema fields
}

class UserRepoMongoDB implements IUserRepo {
  createUser(name: string, age: number): Promise<User> {
    // 1. Store user into MongoDB.
    // 2. Convert result from MongoDBUserSchema type to User type and return.
  }
}

export { UserRepoMongoDB };
Enter fullscreen mode Exit fullscreen mode
// file ./UserService.ts
import { IUserRepo } from "./IUserRepo";

class UserService {
  constructor(private userRepo: IUserRepo) {}

  createUser() {
    return this.userRepo.createUser("Max Mustermann", 20);
  }
}

export { UserService };
Enter fullscreen mode Exit fullscreen mode
// file ./main.ts
import { UserRepoMongoDB } from "./UserRepoMongoDB";
import { UserService } from "./UserService";

function executeCode() {
  new UserService(new UserRepoMongoDB()).createUser();
}
Enter fullscreen mode Exit fullscreen mode

When DI comes to the rescue

One day the team decides to use DynamoDB instead of MongoDB, so we must change the code to make it works. Without DI, we need to create a new class UserRepoDynamoDB with the createUser() function and change our UserService to use this new function. That means we have to change our core business logic code (UserService) every time there is an update on the database module.

// file ./UserRepoDynamoDB.ts
interface DynamoDBUserSchema {
  // DynamoDB user schema fields
}

class UserRepoDynamoDB {
  createUser(
    Id: string, // DynamoDB needs this field in user Table
    name: string,
    age: number
  ): Promise<DynamoDBUserSchema> {
    // store user data in DynamoDB
  }
}

export { UserRepoDynamoDB };
Enter fullscreen mode Exit fullscreen mode
// file ./UserService.ts
import { randomUUID } from "crypto";
import { UserRepoDynamoDB } from "./UserRepoDynamoDB";

class UserService {
  // This function is updated to adapt DynamoDB
  createUser() {
    const Id = randomUUID();
    return new UserRepoDynamoDB().createUser(Id, "Max Mustermann", 20);
  }
}

export { UserService };
Enter fullscreen mode Exit fullscreen mode

But if we use DI, all we need to do is make the new class UserRepoDynamoDB implement IUserRepo, and that's it!
There is no need to change anything in UserService.

// file ./UserRepoDynamoDB.ts
import { IUserRepo, User } from "./IUserRepo";

interface DynamoDBUserSchema {
  // DynamoDB user schema fields
}

class UserRepoDynamoDB implements IUserRepo {
  createUser(name: string, age: number): Promise<User> {
    // 1. Generate new Id and Store user into DynamoDB.
    // 2. Convert result from DynamoDBUserSchema type to User type and return.
  }
}

export { UserRepoDynamoDB };
Enter fullscreen mode Exit fullscreen mode
// file ./main.ts
import { UserRepoDynamoDB } from "./UserRepoDynamoDB";
import { UserService } from "./UserService";

function executeCode() {
  // Use UserRepoDynamoDB instead of old UserRepoMongoDB
  new UserService(new UserRepoDynamoDB()).createUser();
}

executeCode();
Enter fullscreen mode Exit fullscreen mode

Tiugo image

Modular, Fast, and Built for Developers

CKEditor 5 gives you full control over your editing experience. A modular architecture means you get high performance, fewer re-renders and a setup that scales with your needs.

Start now

Top comments (0)

Image of PulumiUP 2025

Let's talk about the current state of cloud and IaC, platform engineering, and security.

Dive into the stories and experiences of innovators and experts, from Startup Founders to Industry Leaders at PulumiUP 2025.

Register Now

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay