Containers
Milan Herda
November, 2025
We will be talking about:
- DI*
- IoC*
- DIP*
- Service Container
- DIC
- Dependency Injection
- Inversion of Control
- Dependency Inversion Principle
- Service Container
- Dependency Injection Container (DI Container)
* covered in my previous presentation about Dependency Injection
What have we learned previously
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.
Container
Chapter 4:
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,
}"Dependencies creation" 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();
const container = {
db: dbConnection,
userRepository,
session,
};
export default container;"Dependencies creation" file
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>,
);
Bootstrap code
We have created a new object holding references to all services our application needs and uses
It contains services...
Service Container
therefore we can call it...
It contains services...
It contains services...
Service Container
- object providing access to other services
- usually created once and used as a singleton
- in order to access a service, each service has to have a name
- container hides the implementation details to its clients. Client does not need to know:
- how the service is created,
- what are its dependencies,
- what implementation it is
- as long as the contract is honored

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
- we make list of services private
- and add two methods
- getService
- registerService
// 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;Usage
Problem?
We need to know the exact order of service creation
Service Container v3
- let's create a service only when something asks for it
- it would allow us to not care about the order creation
- it would also save some resources (like db connections until they are needed, etc)
// 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;Usage
Problem?
What if there are circular dependencies?
Service Container v4
- let's add a check for circular dependencies
- it manifests itself when container is asked for a service and during its creation it is asked for it once again
// 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;Usage (same as before)
Problem?
Browser bundle size
Service Container v5
- let's not import everything right away
- add runtime import support
// 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;Usage
Problem?
Plenty of them
Other problems to solve
Depending on your use case:
- Proper TypeScript support
- Do not sent server code to browser
- React Server Components
- How to use async getService method and non-async React components together
- ...
Service Locator
Chapter 5:

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
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.
DI Container
Chapter 6:
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:
-
autowiring of provided services
- container will read parameters and will construct factory method by itself with the correct arguments
-
autodiscovery of services
- container will look into provided folders, register all services it finds and autowires them
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
Configuration
Chapter 7:
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
What have we learned so far
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.
What have we learned so far
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 befetits
Main benefits of having a Service Container are:
- dependencies of every service are visible from its parameters
- we can replace implementation of a service without the need to touch the rest of the application
- providing we honor the same contract

Thank you!
Any questions?
Containers
By Milan Herda
Containers
- 42