What is Inversion of Control (IoC) anyway?
So, practically speaking, it’s when someone else creates and gives me the things I need instead of creating them myself.
Imagine you are a developer who needs tasks to work on. Instead of going out and choosing tasks yourself, a project manager assigns them to you.
This is Inversion of Control in action: You don’t control how the tasks arrive — that responsibility is delegated to someone else. You just focus on doing the work.
Similarly, in software, IoC means that components don’t create or manage their own dependencies — they get them from elsewhere, such as a container or a framework.
This makes the system more decoupled, testable, and flexible, because parts of the system don’t need to know how to set up other parts.
Alright, let’s see it in the code.
⚠️This is terrible code!
class TaskRepository {
constructor(private db: MySql) { }
findTasks(query: Query): Task[] {
return this.db.fetchAll(query).map((row) => Task.create(row))
}
}
class Backlog {
private repository: TaskRepository;
constructor(db: MySql) {
this.repository = new TaskRepository(db);
}
getTasks(): Task[] {
return this.repository.findTasks(onlyEstemated());
}
}
class Developer {
private backlog: Backlog;
constructor(db: MySql) {
this.backlog = new Backlog(db);
}
work() {
this.backlog.getTasks().forEach(task => task.done());
}
}
const developer = new Developer(MySql.connect());
developer.work();
In this example, the database connection is created at the top level and passed manually through every layer. Each component constructs its own dependencies and directly depends on the database. This results in tight coupling and low flexibility — every component is aware of and tied to the database.
If I ever want to change the source of my tasks (e.g., from MySQL to S3), I’ll need to modify multiple components. That’s brittle and hard to maintain.
👍This is a much better approach!
import { Container } from '@ivorobioff/ioc-container';
class TaskRepository {
private source: Source;
constructor(container: Container) {
this.source = container.get('source');
}
findTasks(query: Query): Task[] {
return this.source.fetchAll(query).map((row) => Task.create(row))
}
}
class Backlog {
constructor(container: Container) {
this.repository = container.get(TaskRepository);
}
getTasks(): Task[] {
return this.repository.findTasks(onlyEstemated());
}
}
class Developer {
private backlog: Backlog;
constructor(container: Container) {
this.backlog = container.get(Backlog);
}
work() {
this.backlog.getTasks().forEach(task => task.done());
}
}
const container = new Container();
container.registerType(Developer);
container.registerType(Backlog);
container.registerType(TaskRepository);
container.registerFactory('source', () => MySql.connect());
const developer = container.get(Developer);
developer.work();
Here, we’ve introduced an IoC (Inversion of Control) container to manage dependencies. Each component depends only on what it truly needs — nothing more, nothing less. Only TaskRepository is aware of the database. All other components remain agnostic to how tasks are fetched.
This means changing the data source (e.g., switching to S3) requires just one small change during system setup:
container.registerFactory('source', () => S3.connect());
Note: Source is an interface, so any compatible implementation (MySQL, S3, etc.) can be easily swapped in. This decouples your code and makes it far more maintainable and extensible.
💡If you’re looking for a lightweight and flexible IoC container to bring this pattern into your own projects, check out @ivorobioff/ioc-container on npm.
It’s minimal, intuitive, and works well in both backend and frontend environments.
For React developers, there’s also @ivorobioff/ioc-react, a seamless integration that makes dependency injection in React components simple and clean.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.