Amblem
Furkan Baytekin

Understanding the Dependency Inversion Principle with TypeScript and Databases

How DIP makes database interactions more flexible & testable in TypeScript

Understanding the Dependency Inversion Principle with TypeScript and Databases
158
3 minutes

The Dependency Inversion Principle (DIP) is a key SOLID principle that helps us write flexible, maintainable code by decoupling high-level logic from low-level details. Instead of hardcoding dependencies, we rely on abstractions. Today, we’ll explore DIP with a practical example: a UserService that works with MongoDB, MySQL, and PostgreSQL, all in TypeScript.

The Problem: Tight Coupling

Imagine a UserService that directly uses a MySQL database. If you later need to switch to MongoDB, you’d rewrite the service. That’s rigid and messy. DIP fixes this by making both the service and the database depend on an abstraction.

Step 1: Define the Abstraction

We start with an interface that defines what our database should do—here, saving a user.

typescript
interface UserRepository { saveUser(user: { id: string; name: string }): Promise<void>; }

This is our contract. The UserService will depend on this, not a specific database.

Step 2: Implement Concrete Repositories

Next, we create implementations for MongoDB, MySQL, and PostgreSQL, each adhering to UserRepository.

MongoDB Repository

typescript
import { MongoClient } from "mongodb"; class MongoUserRepository implements UserRepository { private client: MongoClient; constructor() { this.client = new MongoClient("mongodb://localhost:27017"); } async saveUser(user: { id: string; name: string }): Promise<void> { const db = this.client.db("mydb"); await db.collection("users").insertOne(user); console.log(`Saved ${user.name} to MongoDB`); } }

MySQL Repository

typescript
import mysql from "mysql2/promise"; class MySQLUserRepository implements UserRepository { private connection: mysql.Connection; constructor() { this.connection = await mysql.createConnection({ host: "localhost", user: "root", database: "mydb", }); } async saveUser(user: { id: string; name: string }): Promise<void> { await this.connection.execute( "INSERT INTO users (id, name) VALUES (?, ?)", [user.id, user.name] ); console.log(`Saved ${user.name} to MySQL`); } }

PostgreSQL Repository

typescript
import { Pool } from "pg"; class PostgresUserRepository implements UserRepository { private pool: Pool; constructor() { this.pool = new Pool({ user: "postgres", host: "localhost", database: "mydb", password: "password", }); } async saveUser(user: { id: string; name: string }): Promise<void> { await this.pool.query( "INSERT INTO users (id, name) VALUES ($1, $2)", [user.id, user.name] ); console.log(`Saved ${user.name} to PostgreSQL`); } }

Each class implements UserRepository, but the details (SQL queries, MongoDB collections) are hidden behind the interface.

Step 3: The High-Level Service

Now, the UserService depends only on the UserRepository abstraction, not the concrete databases.

typescript
class UserService { private repository: UserRepository; constructor(repository: UserRepository) { this.repository = repository; // Dependency injected } async addUser(user: { id: string; name: string }) { await this.repository.saveUser(user); } }

Step 4: Putting It Together

We can now swap databases without touching UserService.

typescript
async function main() { const user = { id: "1", name: "Alice" }; // Use MongoDB const mongoRepo = new MongoUserRepository(); const mongoService = new UserService(mongoRepo); await mongoService.addUser(user); // Switch to MySQL const mysqlRepo = new MySQLUserRepository(); const mysqlService = new UserService(mysqlRepo); await mysqlService.addUser(user); // Switch to PostgreSQL const postgresRepo = new PostgresUserRepository(); const postgresService = new UserService(postgresRepo); await postgresService.addUser(user); } main().catch(console.error);

Why DIP Shines Here

Wrapping Up

DIP turns dependencies upside down: instead of UserService dictating a specific database, both rely on the UserRepository interface. This small shift makes your TypeScript apps more modular and adaptable. Try it out next time you’re wiring up a service—your future self will thank you!


Album of the day:

Suggested Blog Posts