There’s a question that comes up, in one form or another, in almost every modern software architecture: who decides when your code runs? In a naive program, the answer is obvious, you do. Your code calls its dependencies, orchestrates them, decides the sequence. But the moment you introduce a framework, a runtime, or an injection container, that balance of power flips: it’s now the third-party entity that calls your code, whenever it sees fit. This reversal has a name, Inversion of Control, and it’s one of the most structuring principles in software design, even though it’s often applied without ever being named.
A bit of history
The IoC principle is anything but recent. Its early traces go back to the 1970s with Michael Jackson (the British computer scientist, not the artist), who at the time talked about program inversion: the idea of reversing a program’s flow of control to make it easier to compose.
The concept was later popularized and formalized by figures like Martin Fowler and Robert C. Martin, and made its way into the famous Gang of Four book on design patterns, a book that, more than thirty years after its publication, remains an essential reference. Far from being a passing fad, IoC is therefore a foundation that has endured across the decades.
The principle of Inversion of Control
IoC consists of delegating the execution and control of a program to a third-party entity: a library, a framework, or a runtime. This shift of responsibility has a very concrete goal: promoting extensibility. It lets you write domain-specific code that will be executed at a time chosen by that third-party entity, according to its own rules, without your code having to orchestrate anything.
To really grasp the reversal, you have to compare it with the classic model. Without IoC, your business code is at the center: it directly manages its dependencies and decides itself when to call them, based on need. You’re the one holding the thread of execution.
IoC inverts this relationship. Your code no longer calls its dependencies: they call it. Hence the image often used to illustrate this principle, the Hollywood Principle: “Don’t call us, we’ll call you.” You don’t contact the third-party entity to demand execution; you hand it a behavior, and it will call you back when the time comes.
In practice, the pattern is always the same: define some specific code that respects a contract, then let a third-party entity handle its execution at the right time. This contract (a function signature, an interface, a lifecycle hook) is the hinge that makes the inversion possible without rigid coupling.
IoC implementations are everywhere
Once you’ve identified this pattern, you recognize it everywhere. IoC isn’t an isolated mechanism: it’s a family of techniques that varies according to the type of entity you delegate control to.
Runtimes: Node.js and callbacks
At the most fundamental level, a callback is a direct expression of IoC. Rather than calling a function ourselves, we hand it to another function, which will decide the right moment to invoke it. The callback is one of the primitives that make it possible to implement IoC, precisely because it relies on the delegation of responsibilities.
In the Node.js ecosystem, this model is everywhere: the runtime takes charge of scheduling and calls your code back when the event occurs or the delay expires.
// IoC with Node.js: delegating execution with Callbacks
// 1. IoC through delegating execution to the runtime
setTimeout(() => myCustomDomainCode(), 10_000);
// 2. IoC through delegating execution to another function
function someFunctionThatCallsDomainCode(callback) {
// [...]
callback();
}
In the first case, you don’t decide when myCustomDomainCode runs: you delegate that decision to the runtime, which will call you back in ten seconds. In the second, it’s someFunctionThatCallsDomainCode that holds control of the flow and chooses the moment to invoke the behavior you handed it.
Frameworks: Angular and component lifecycles
A framework goes further than a simple library: its role is to establish a structure and abstract away part of the complexity by offering ready-to-use features. This structure necessarily imposes a form of IoC, because it’s the framework that drives the flow of the application.
Angular is a good example. A component’s lifecycle (its creation, its rendering, its destruction) is managed internally by the framework. You don’t control it directly. However, thanks to IoC, you can hook code specific to your application onto the various stages of that lifecycle: the framework will invoke your methods at the right time.
// IoC with Angular: managing a component's lifecycle
import { Component } from "@angular/core";
@Component({
/* ... */
})
export class SomeComponent {
ngOnInit() {
myCustomDomainCode();
}
ngOnDestroy() {
myCustomDomainCode();
}
}
Here, ngOnInit and ngOnDestroy are lifecycle hooks. You never call them yourself: Angular invokes them when the component is initialized and destroyed. The contract (“implement these methods and I’ll trigger them at the right time”) is exactly what makes the inversion possible.
Dependency injection
Dependency injection is probably the best-known implementation of the IoC principle, to the point that the two are sometimes confused. Yet it’s a particular case: delegating the responsibility for the creation and management of dependencies to a third-party entity, rather than letting a component instantiate what it needs itself.
The key, here again, is respecting an interface contract. By depending on an abstraction rather than a concrete implementation, you guarantee far better modularity and greater flexibility: the actual implementation can be swapped out without touching the code that consumes it.
// Dependency Injection
interface Logger {
log(message: string): void;
}
class FileSystemReader {
constructor(private readonly logger: Logger) {}
readFile(path: string) {
this.logger.log(path);
}
}
const myCustomLogger: Logger = {
log: myCustomDomainCode,
};
const fileReader = new FileSystemReader(myCustomLogger);
FileSystemReader knows nothing about how the log is actually written: it depends only on the Logger interface. It’s the outside world that decides on the concrete implementation and injects it through the constructor. The inversion is back: the component doesn’t go looking for its dependency, it receives it. This property is what makes this kind of code easy to test (by injecting a fake logger) and to evolve (by changing the implementation without breaking anything elsewhere).
Conclusion
From a setTimeout callback to a framework’s lifecycle hooks, by way of dependency injection, it’s always the same principle at work: handing control of the execution flow to a third-party entity, and keeping for yourself only the business behavior. IoC is less an isolated pattern than a way of thinking about architecture, one that separates what your code does from when and how it gets invoked.
It’s precisely this separation that makes modern systems extensible, modular, and testable. Once you’ve learned to recognize “Don’t call us, we’ll call you,” you see it at work in just about every tool you use day to day.