Milan Herda
November, 2025
* covered in my previous presentation about Dependency Injection
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.
I want to display the name of the currently logged in user.
import type { DbQuery } from "./db";
import type { User, UserData, UserFactoryFunc } from "./userFactory";
export interface UserRepository {
getUser: (userId: number) => User;
getAllUsers: () => User[];
}
export default function createUserRepository(
db: DbQuery,
createUser: UserFactoryFunc,
) {
const repo: UserRepository = {
getUser(userId: number): User {
// ...
},
getAllUsers(): User[] {
const rows = db.select("*").from("users").execute<UserData>();
if (!rows) {
throw new Error("User not found");
}
return rows.map((row) => createUser(row));
},
};
return repo;
}
UserRepository from previous lesson
function App() {
const user = userRepository.getUser(session.userId);
return (
<>
<h1>User data</h1>
<div className="card">{user.getFullname()}</div>
</>
);
}
export default App;
Our App code
Where do we get the userRepository from?
It is our dependency, so we should ask for it in function arguments
import type { UserSession } from "./services/session";
import type { UserRepository } from "./services/userRepository";
function App({
userRepository,
session,
}: {
userRepository: UserRepository;
session: UserSession;
}) {
const user = userRepository.getUser(session.userId);
return (
<>
<h1>User data</h1>
<div className="card">{user.getFullname()}</div>
</>
);
}
export default App;
Our App code
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.
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);
Our bootstrap code
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App
userRepository={userRepository}
session={session}
/>
</StrictMode>,
);
Our bootstrap code
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import createSession from "./services/session";
const session = createSession();
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App
userRepository={userRepository}
session={session}
/>
</StrictMode>,
);
Our bootstrap code
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import createSession from "./services/session";
import createUserRepository from "./services/userRepository";
const userRepository = createUserRepository(dbConnection, createUser);
const session = createSession();
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App
userRepository={userRepository}
session={session}
/>
</StrictMode>,
);
Our bootstrap code
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import createSession from "./services/session";
import createUserRepository from "./services/userRepository";
import createConnection from "./services/db";
import createUser from "./services/userFactory";
const dbDsn = "mysql://user:password@localhost/myDb";
const dbConnection = createConnection(dbDsn);
const userRepository = createUserRepository(dbConnection, createUser);
const session = createSession();
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App
userRepository={userRepository}
session={session}
/>
</StrictMode>,
);
Our bootstrap code
How many responsibilities does this code have?
Rendering the application
Construction of dependencies
What we usually do with multiple responsibilities?
We split them to their own functions, or modules.
Let's separate depedencies creation to its own file
import createSession from "./services/session";
import createUserRepository from "./services/userRepository";
import createConnection from "./services/db";
import createUser from "./services/userFactory";
const dbDsn = "mysql://user:password@localhost/myDb";
const dbConnection = createConnection(dbDsn);
const userRepository = createUserRepository(dbConnection, createUser);
const session = createSession();
export default {
db: dbConnection,
userRepository,
session,
}import createSession from "./services/session";
import createUserRepository from "./services/userRepository";
import createConnection from "./services/db";
import createUser from "./services/userFactory";
const dbDsn = "mysql://user:password@localhost/myDb";
const dbConnection = createConnection(dbDsn);
const userRepository = createUserRepository(dbConnection, createUser);
const session = createSession();
const container = {
db: dbConnection,
userRepository,
session,
};
export default container;import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import container from './container';
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App
userRepository={container.userRepository}
session={container.session}
/>
</StrictMode>,
);
We have created a new object holding references to all services our application needs and uses
It contains services...
therefore we can call it...
It contains services...
It contains services...
Service Container
Should I just create a container and put all services of my application there?
Yes
What if I have like, hundreds or thousands of them?
It would be even better
How it can be better? Would not it be a huge object?
Yes
Service Containers are usually not implemented as simple objects.
They provide services through access methods, which allow them to use some optimization techniques.
Service Container v2
// src/framework/createContainer.ts
interface ServiceContainer {
getService(name: string): any;
}
interface MutableServiceContainer extends ServiceContainer {
registerService(name: string, service: any): MutableServiceContainer;
}
function createContainer(): MutableServiceContainer {
const services: Record<string, any> = {};
return {
registerService(name, service) {
services[name] = service;
return this;
},
getService(name) {
if (services[name]) {
return services[name];
}
throw new Error(`No service with name ${name}`);
},
};
}
export default createContainer;Service Container v2
// src/container.ts
import createContainer from "./framework/containerV2";
// ... other imports
const container = createContainer();
const dbDsn = "mysql://user:password@localhost/myDb";
const db = createConnection(dbDsn);
container.registerService("userFactory", createUser);
container.registerService("db", db);
container.registerService(
"userRepository",
createUserRepository(db, createUser),
);
container.registerService("session", createSession());
export default container;Problem?
We need to know the exact order of service creation
Service Container v3
// src/framework/createContainer.ts
interface ServiceContainer {
getService(name: string): any;
}
interface MutableServiceContainer extends ServiceContainer {
registerService(name: string, service: any): MutableServiceContainer;
}
function createContainer(): MutableServiceContainer {
const services: Record<string, any> = {};
return {
registerService(name, service) {
services[name] = service;
return this;
},
getService(name) {
if (services[name]) {
return services[name];
}
throw new Error(`No service with name ${name}`);
},
};
}
export default createContainer;Service Container v3
// src/framework/createContainer.ts
interface ServiceContainer {
getService(name: string): any;
}
type ServiceFactoryFunc = (container: ServiceContainer) => any;
interface MutableServiceContainer extends ServiceContainer {
registerService(
name: string,
serviceFactory: ServiceFactoryFunc,
): MutableServiceContainer;
}
function createContainer(): MutableServiceContainer {
const services: Record<string, any> = {};
return {
registerService(name, service) {
services[name] = service;
return this;
},
getService(name) {
if (services[name]) {
return services[name];
}
throw new Error(`No service with name ${name}`);
},
};
}
export default createContainer;Service Container v3
// src/framework/createContainer.ts
//...
type ServiceFactoryFunc = (container: ServiceContainer) => any;
// ...
function createContainer(): MutableServiceContainer {
const services: Record<string, any> = {};
const factories: Record<string, ServiceFactoryFunc> = {};
return {
registerService(name, serviceFactory) {
factories[name] = serviceFactory;
return this;
},
getService(name) {
if (services[name]) {
return services[name];
}
if (factories[name]) {
services[name] = factories[name](this);
return services[name];
}
throw new Error(`No service with name ${name}`);
},
};
}
export default createContainer;Service Container v3
// src/container.ts
import createContainer from "./framework/containerV2";
// ... other imports
const container = createContainer();
const dbDsn = "mysql://user:password@localhost/myDb";
container.registerService("userRepository", (container) => {
return createUserRepository(
container.getService("db"),
container.getService("userFactory"),
);
});
container.registerService("userFactory", () => {
return createUser;
});
container.registerService("db", () => {
return createConnection(dbDsn);
});
container.registerService("session", () => {
return createSession();
});
export default container;Problem?
What if there are circular dependencies?
Service Container v4
// src/framework/createContainer.ts
function createContainer(): MutableServiceContainer {
// ...
return {
// ...
getService(name) {
if (services[name]) {
return services[name];
}
if (factories[name]) {
services[name] = factories[name](this);
return services[name];
}
throw new Error(`No service with name ${name}`);
},
};
}Service Container v4
// src/framework/createContainer.ts
function createContainer(): MutableServiceContainer {
// ...
const waitingStack: string[] = [];
return {
// ...
getService(name) {
if (services[name]) {
return services[name];
}
if (waitingStack.find((n) => n === name)) {
throw new Error(
`Circular dependencies between [${waitingStack.join(",")}, ${name}]`,
);
}
if (factories[name]) {
waitingStack.push(name);
services[name] = factories[name](this);
waitingStack.pop();
return services[name];
}
throw new Error(`No service with name ${name}`);
},
};
}Service Container v4
// src/container.ts
import createContainer from "./framework/containerV2";
// ... other imports
const container = createContainer();
const dbDsn = "mysql://user:password@localhost/myDb";
container.registerService("userRepository", (container) => {
return createUserRepository(
container.getService("db"),
container.getService("userFactory"),
);
});
container.registerService("userFactory", () => {
return createUser;
});
container.registerService("db", () => {
return createConnection(dbDsn);
});
container.registerService("session", () => {
return createSession();
});
export default container;Problem?
Browser bundle size
Service Container v5
// src/framework/createContainer.ts
type ServiceFactoryFunc = (container: ServiceContainer) => Promise<any>;
function createContainer(): MutableServiceContainer {
// ...
return {
// ...
async getService(name) {
if (services[name]) {
return services[name];
}
if (waitingStack.find((n) => n === name)) {
throw new Error(
`Circular dependencies between [${waitingStack.join(",")}, ${name}]`,
);
}
if (factories[name]) {
waitingStack.push(name);
services[name] = await factories[name](this);
waitingStack.pop();
return services[name];
}
throw new Error(`No service with name ${name}`);
},
};
}Service Container v5
// src/container.ts
import createContainer from "./framework/containerV2";
const container = createContainer();
container.registerService("userRepository", async (container) => {
const { default: createUserRepository } = await import(
"./services/userRepository"
);
return createUserRepository(
await container.getService("db"),
await container.getService("userFactory"),
);
});
container.registerService("userFactory", async () => {
const { default: createUser } = await import("./services/userFactory");
return createUser;
});
// ...
export default container;Problem?
Plenty of them
Depending on your use case:
Container is created at the top of my application.
I need a service from it in a component half-a-bazillion layers down.
Do I have to pass the service down through all intermediate layers?
No.
You can import container there and ask for services you need
// src/ui/User/Dashboard/Badges.ts
import container from '@local/container';
export default async function UserBadges() {
const session = await container.getService('session');
const badgesRepository = await container.getService('badgesRepository');
const badges = badgesRepository.getUserBadges(session.userId);
// ...
}Service Locator
We are trying to locate the service in the code of our function.
And we are locating it using the Service Container
So we have all the benefits of not having to care about implementations, lazy loading etc.
Looks great
?
Service Locator
This is an antipattern. Avoid it if you can.
// src/ui/User/Dashboard/Badges.ts
import container from '@local/container';
export default async function UserBadges() {
const session = await container.getService('session');
const badgesRepository = await container.getService('badgesRepository');
const badges = badgesRepository.getUserBadges(session.userId);
// ...
}What does this function need to work properly?
// src/ui/User/Dashboard/Badges.ts
import container from '@local/container';
export default async function UserBadges() {
// ...
}What does this function need to work properly?
We have absolutely no idea.
Dependency Injection Container
DI Container
Implementation of Service Container
DI Container
Implementation of Service Container
DI Container
Usage of Service Container
In such a way that almost all services are registered
And their dependencies provided as arguments are also obtained from container
Its code does not have to be different from what we already saw
// src/container.ts
import createContainer from "./framework/container";
const container = createContainer();
container.registerService("userRepository", (container) => {
return createUserRepository(
container.getService("db"),
container.getService("userFactory"),
);
});
// ...DI Container
But DI Containers usually provide more advanced features like:
Most DI Containers in JS work only with classes
Autowiring is handled with the help of decorators
Autowiring for factory functions or Module Pattern modules can be solved by bundler, custom configuration or by comparing TypeScript shapes
The code needed for service registration is often very similar
We can take advantage of that and make registration less verbose for developers
container.registerService("userRepository", (container) => {
return createUserRepository(
container.getService("db"),
container.getService("userFactory"),
);
});userRepository:
path: '@local/service/userRepository'
params:
db: @db
createUser: @userFactorycontainer.registerService("userRepository", (container) => {
return createUserRepository(
container.getService("db"),
container.getService("userFactory"),
);
});userRepository:
path: '@local/service/userRepository'
With autowiring in place
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.
Service Locator is using the Container in a bad way. Having only half of the benefits and hiding function dependencies.
Service Container is an object encapsulating all services your application needs and providing access to them.
Dependency Injection Container is advanced usage of a Service Container where it handles injection of dependencies for all services.
Main benefits of having a Service Container are:
Thank you!
Any questions?