Milan Herda
October, 2025
// src/services/userRepository.ts
export function getCurrentUser(): User {
// ...
}
What does this function need to work properly?
// src/services/userRepository.ts
export function getCurrentUser(): User {
// ...
}
What does this function need to work properly?
We do not know.
It is not written in its documentation, nor declaration
// src/services/userRepository.ts
export function getCurrentUser(): User {
const db = createDbConnection("mysql://user:password@localhost:3306/mydb");
const rows = db
.select("*")
.from("users")
.where("id = :id", { id: session.userId })
.execute<UserData>();
if (rows.length === 0) {
throw new Error("User not found");
}
const userData = rows[0];
return {
id: userData.id,
name: userData.name,
surname: userData.surname,
email: userData.email,
getFullName() {
return `${this.name} ${this.surname}`;
},
};
}
What does this function need to work properly?
What does this function need to work properly?
But besides of that...
// src/services/userRepository.ts
export function getCurrentUser(): User {
const db = createDbConnection("mysql://user:password@localhost:3306/mydb");
const rows = db
.select("*")
.from("users")
.where("id = :id", { id: session.userId })
.execute<UserData>();
if (rows.length === 0) {
throw new Error("User not found");
}
const userData = rows[0];
return {
id: userData.id,
name: userData.name,
surname: userData.surname,
email: userData.email,
getFullName() {
return `${this.name} ${this.surname}`;
},
};
}
DB Query object
Session object
User entity creation logic
DSN string
provided by foreign function
Our function needs those objects and functionality.
It requires them in order to work properly.
It depends on them.
They are its dependencies.
// src/services/userRepository.ts
export function getCurrentUser(): User {
const db = createDbConnection("mysql://user:password@localhost:3306/mydb");
const rows = db
.select("*")
.from("users")
.where("id = :id", { id: session.userId })
.execute<UserData>();
if (rows.length === 0) {
throw new Error("User not found");
}
const userData = rows[0];
return {
id: userData.id,
name: userData.name,
surname: userData.surname,
email: userData.email,
getFullName() {
return `${this.name} ${this.surname}`;
},
};
}
Problems?
// src/services/userRepository.ts
export function getCurrentUser(): User {
// ...
}Problems?
Why do programmers tend to write code in this way?
This one is a lie we tell ourselves
// src/services/userRepository.ts
export function getCurrentUser(): User {
const db = createDbConnection("mysql://user:password@localhost:3306/mydb");
const rows = db
.select("*")
.from("users")
.where("id = :id", { id: session.userId })
.execute<UserData>();
if (rows.length === 0) {
throw new Error("User not found");
}
const userData = rows[0];
return {
id: userData.id,
name: userData.name,
surname: userData.surname,
email: userData.email,
getFullName() {
return `${this.name} ${this.surname}`;
},
};
}
What is in control over dependencies?
This function is in control
The function is in control
But we ultimately create applications (or libraries), not single functions
If functions are in control, ...
...does it mean...
...that my application is...
We should invert this control
Inversion of Control
Inversion of Control
We ask clients (callers) to inject those dependencies to our function
// src/services/userRepository.ts
export function getCurrentUser(db: DbQuery, session: Session): User {
const rows = db
.select("*")
.from("users")
.where("id = :id", { id: session.userId })
.execute<UserData>();
if (rows.length === 0) {
throw new Error("User not found");
}
const userData = rows[0];
return {
id: userData.id,
name: userData.name,
surname: userData.surname,
email: userData.email,
getFullName() {
return `${this.name} ${this.surname}`;
},
};
}
Inverting dependencies
// src/somewhere/else.ts
import session from './services/session';
import { getCurrentUser } from './services/userRepository';
const db = createDbConnection("mysql://user:password@localhost:3306/mydb");
const currentUser = getCurrentUser(db, session)
Injecting dependencies
// src/services/userRepository.ts
export function getCurrentUser(db: DbQuery, session: Session): User {
const rows = db
.select("*")
.from("users")
.where("id = :id", { id: session.userId })
.execute<UserData>();
if (rows.length === 0) {
throw new Error("User not found");
}
const userData = rows[0];
return {
id: userData.id,
name: userData.name,
surname: userData.surname,
email: userData.email,
getFullName() {
return `${this.name} ${this.surname}`;
},
};
}
Inverting dependencies
Do we need session, or just userId?
// src/services/userRepository.ts
export function getCurrentUser(db: DbQuery, userId: number): User {
const rows = db
.select("*")
.from("users")
.where("id = :id", { id: userId })
.execute<UserData>();
if (rows.length === 0) {
throw new Error("User not found");
}
const userData = rows[0];
return {
id: userData.id,
name: userData.name,
surname: userData.surname,
email: userData.email,
getFullName() {
return `${this.name} ${this.surname}`;
},
};
}
Inverting dependencies
What to do with this "algorithm" dependency?
// src/services/userRepository.ts
export function getCurrentUser(db: DbQuery, userId: number): User {
const rows = db
.select("*")
.from("users")
.where("id = :id", { id: userId })
.execute<UserData>();
if (rows.length === 0) {
throw new Error("User not found");
}
return createUser(rows[0]);
}
function createUser(data: UserData) {
return {
id: userData.id,
name: userData.name,
surname: userData.surname,
email: userData.email,
getFullName() {
return `${this.name} ${this.surname}`;
},
};
}
Inverting dependencies
We can apply
Single Responsibility Principle*
*extraction to its own function
We start with basic techniques
And ask SOLID principles for help
// src/services/userRepository.ts
export function getCurrentUser(
db: DbQuery,
userId: number,
createUser: UserFactoryFunc
): User {
const rows = db
.select("*")
.from("users")
.where("id = :id", { id: userId })
.execute<UserData>();
if (rows.length === 0) {
throw new Error("User not found");
}
return createUser(rows[0]);
}
Inverting dependencies
If it makes sense, the extracted function can be moved to its own module (file).
When this happens, we know that such function was actually a hidden dependency right from the start
// src/somewhere/else.ts
import session from './services/session';
import createUser from './services/userFactory';
import { getCurrentUser } from './services/userRepository';
const db = createDbConnection("mysql://user:password@localhost:3306/mydb");
const currentUser = getCurrentUser(db, session.userId, createUser);
Injecting dependencies
Is it okay to have so many parameters in my function?
Probably not*
*Unless it is a factory function, or constructor method
The real code is usually more complex and allow us to refactor it
If not, we can still make use of closures
and/or currying
// src/somewhere/else.ts
import session from './services/session';
import createUser from './services/userFactory';
import { getCurrentUser } from './services/userRepository';
const db = createDbConnection("mysql://user:password@localhost:3306/mydb");
const currentUser = getCurrentUser(db, session.userId, createUser);
Injecting dependencies
// src/somewhere/else.ts
import session from './services/session';
import createUser from './services/userFactory';
import { getCurrentUser as getCurrentUserOriginal } from './services/userRepository';
const db = createDbConnection("mysql://user:password@localhost:3306/mydb");
function getCurrentUser(userId: number) {
return getCurrentUserOriginal(db, userId, createUser);
}
const currentUser = getCurrentUser(session.userId);
Injecting dependencies
We can move this wrapper function to its own file and import on all places instead of the original
In our case, userRepository provides more methods
// src/services/userRepository.ts
export function getCurrentUser(
db: DbQuery,
userId: number,
createUser: UserFactoryFunc
): User {
const rows = db
.select("*")
.from("users")
.where("id = :id", { id: userId })
.execute<UserData>();
if (rows.length === 0) {
throw new Error("User not found");
}
return createUser(rows[0]);
}
export function getAllUsers(
db: DbQuery,
createUser: UserFactoryFunc
): User[] {
const rows = db
.select("*")
.from("users")
.execute<UserData>();
return rows.map((row) => createUser(row));
}
In our case, userRepository provides more methods
// src/services/userRepository.ts
export function getUser(
db: DbQuery,
userId: number,
createUser: UserFactoryFunc
): User {
const rows = db
.select("*")
.from("users")
.where("id = :id", { id: userId })
.execute<UserData>();
if (rows.length === 0) {
throw new Error("User not found");
}
return createUser(rows[0]);
}
export function getAllUsers(
db: DbQuery,
createUser: UserFactoryFunc
): User[] {
const rows = db
.select("*")
.from("users")
.execute<UserData>();
return rows.map((row) => createUser(row));
}
In our case, userRepository provides more methods
// src/services/userRepository.ts
export default function createUserRepository(
db: DbQuery,
createUser: UserFactoryFunc
) {
return {
getUser(userId: number): User {
const rows = db
.select("*")
.from("users")
.where("id = :id", { id: userId })
.execute<UserData>();
if (rows.length === 0) {
throw new Error("User not found");
}
return createUser(rows[0]);
},
getAllUsers(): User[] {
const rows = db
.select("*")
.from("users")
.execute<UserData>();
return rows.map((row) => createUser(row));
}
}
}
// src/somewhere/else.ts
import session from './services/session';
import createUser from './services/userFactory';
import createUserRepository from './services/userRepository';
const db = createDbConnection("mysql://user:password@localhost:3306/mydb");
const userRepository = createUserRepository(db, createUser);
const currentUser = userRepository.getUser(session.userId);
Injecting dependencies
// src/services/userRepository.ts
export default function createUserRepository(
db: DbQuery,
createUser: UserFactoryFunc
) {
return {
getUser(userId: number): User {
const rows = db
.select("*")
.from("users")
.where("id = :id", { id: userId })
.execute<UserData>();
if (rows.length === 0) {
throw new Error("User not found");
}
return createUser(rows[0]);
},
getAllUsers(): User[] {
const rows = db
.select("*")
.from("users")
.execute<UserData>();
return rows.map((row) => createUser(row));
}
}
}
Some of you, may have written the repository not like this
But more like this
// src/services/userRepository.ts
import db from './services/dbConnection';
import createUser from './services/userFactory';
export default function createUserRepository() {
return {
getUser(userId: number): User {
const rows = db
.select("*")
.from("users")
.where("id = :id", { id: userId })
.execute<UserData>();
if (rows.length === 0) {
throw new Error("User not found");
}
return createUser(rows[0]);
},
getAllUsers(): User[] {
const rows = db
.select("*")
.from("users")
.execute<UserData>();
return rows.map((row) => createUser(row));
}
}
}
While it works, there is one problem
The code now depends on concrete implementations for database query and user factory.
In case we want to replace them, we would have to modify the userRepository code
Dependency Inversion Principle
Imagine you want to write unit tests for this code
// src/services/userRepository.ts
import db from './services/dbConnection';
import createUser from './services/userFactory';
export default function createUserRepository() {
return {
getUser(userId: number): User {
const rows = db
.select("*")
.from("users")
.where("id = :id", { id: userId })
.execute<UserData>();
if (rows.length === 0) {
throw new Error("User not found");
}
return createUser(rows[0]);
},
getAllUsers(): User[] {
const rows = db
.select("*")
.from("users")
.execute<UserData>();
return rows.map((row) => createUser(row));
}
}
}
Impossible without mocking these
// src/services/userRepository.ts
export default function createUserRepository(
db: DbQuery,
createUser: UserFactoryFunc
) {
return {
getUser(userId: number): User {
const rows = db
.select("*")
.from("users")
.where("id = :id", { id: userId })
.execute<UserData>();
if (rows.length === 0) {
throw new Error("User not found");
}
return createUser(rows[0]);
},
getAllUsers(): User[] {
const rows = db
.select("*")
.from("users")
.execute<UserData>();
return rows.map((row) => createUser(row));
}
}
}
Do we need module mocking in unit tests for this?
No
We can provide our own implementations.
We just have to fulfill the minimum contract
Unit tests are just an example of requirement to provide different implementation.
This requirement is already an integral part of all of our code.
If we stick to Dependency Inversion Principle and depend only on abstractions, then our code is not only easier to test, but also easier to modify
Inversion of Control is about taking control from small units and giving it to the bigger units (application, or framework in the end).
Dependency Injection is about providing dependencies to functions, objects or modules. It is just passing arguments to functions.
Dependency Inversion Principle tells us to depend on abstractions, not on concrete implementations.
When we meticulously apply IoC, DI and DIP, where does it end?
When all functions are asking for dependencies, something somewhere must be ultimately responsible for creation of all of them.
Next Time
Thank you!
Any questions?