Disclaimer: ChatGPT generated document.
Inversion of Control (IoC) is a software-design principle in which a component does not control the overall flow of execution or the creation/selection of its dependencies. Instead, some external mechanism—such as a framework, container, runtime, event loop, or orchestrator—controls those things and calls your code at the appropriate time.
The shortest useful description is:
Traditional code: your code calls the library. IoC: the framework/runtime calls your code.
That simple reversal has enormous consequences for architecture, testing, extensibility, dependency management, frameworks, plugins, event-driven systems, and modern application development.
Consider an ordinary program:
main()
├── read configuration
├── create database
├── create repository
├── create service
├── call service
└── print result
Your application explicitly determines:
- what objects exist,
- when they are created,
- how they are connected,
- what runs next.
For example:
class OrderService {
private final MySqlOrderRepository repository =
new MySqlOrderRepository();
public void placeOrder(Order order) {
repository.save(order);
}
}OrderService controls its dependency.
It decides:
I need a repository
↓
I'll use MySqlOrderRepository
↓
I'll construct it myself
↓
I'll call it myself
That creates strong coupling.
With IoC, some of those decisions move outside the component:
class OrderService {
private final OrderRepository repository;
OrderService(OrderRepository repository) {
this.repository = repository;
}
public void placeOrder(Order order) {
repository.save(order);
}
}Now:
external mechanism
│
│ provides
▼
OrderService ───► OrderRepository
OrderService no longer decides which repository implementation gets created.
That's an inversion of control.
This is where the term can become confusing.
There isn't one single type of control that IoC must invert.
Different architectures invert different forms of control.
Instead of:
var repository = new SqlRepository();
var service = new UserService(repository);a container constructs everything.
Instead of:
PaymentGateway gateway = new StripeGateway();something external decides which implementation satisfies:
PaymentGatewayInstead of your code deciding:
do A
then B
then C
a framework decides when your functions run.
The framework may determine when components:
start
initialize
receive work
stop
dispose
Your program registers handlers:
button.addEventListener("click", handleClick);The browser decides when handleClick() executes.
Your code doesn't do:
while (true) {
if (button.wasClicked()) {
handleClick();
}
}The runtime owns the main event loop.
That is IoC too.
IoC is closely associated with the phrase:
"Don't call us, we'll call you."
Imagine a framework saying:
Developer:
"When should I call the framework?"
Framework:
"You don't.
Give me your components and callbacks.
I'll call them when they're needed."
This is sometimes called the Hollywood Principle.
It captures control-flow inversion particularly well.
This distinction provides one of the easiest ways to understand IoC.
Your program controls execution:
import math
x = math.sqrt(25)The application says:
My program
│
├── calls library
▼
Library
The framework often controls execution:
@app.get("/users")
def users():
return get_users()You don't normally write:
while True:
request = wait_for_http_request()
if request.path == "/users":
users()The framework/runtime does something conceptually like that.
Framework
│
│ invokes
▼
Your code
That is a classic manifestation of IoC.
A useful heuristic is therefore:
Libraries are generally called by applications; frameworks generally call application code.
It's not an absolute technical definition, but it's an excellent mental model.
This distinction is extremely important.
People often say:
IoC = Dependency Injection
That's inaccurate.
Dependency Injection (DI) is one technique for implementing IoC.
Think:
Inversion of Control
│
┌───────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
Dependency Event-driven Template
Injection callbacks Method
│
▼
Constructor / Setter / Field injection
IoC is the broader principle.
DI specifically concerns supplying dependencies from outside a component.
Suppose:
class CheckoutService {
private StripePaymentGateway gateway =
new StripePaymentGateway();
}The class determines its dependency.
We can invert that responsibility:
class CheckoutService {
private final PaymentGateway gateway;
CheckoutService(PaymentGateway gateway) {
this.gateway = gateway;
}
}Something outside supplies:
new CheckoutService(
new StripePaymentGateway()
);This is constructor injection.
The important transformation is:
Before
CheckoutService
│
│ constructs
▼
StripePaymentGateway
versus:
After
Composition Root
│
├──── constructs ────► StripePaymentGateway
│
└──── constructs ────► CheckoutService
▲
│ receives dependency
The service becomes less concerned with object construction and more concerned with its actual business responsibility.
Another common misconception:
Dependency Injection requires a DI framework.
It doesn't.
This is perfectly valid DI:
class UserService:
def __init__(self, repository):
self.repository = repository
repository = PostgresUserRepository()
service = UserService(repository)The programmer manually performs the injection.
This is often called:
manual dependency injection or pure DI.
A DI container merely automates some of the composition.
An IoC container is infrastructure that manages components and their dependencies.
Suppose you have:
Controller
↓
Service
↓
Repository
↓
Database
Instead of constructing them manually:
Database db = new Database(config);
Repository repo =
new SqlRepository(db);
UserService service =
new UserService(repo);
UserController controller =
new UserController(service);a container can maintain something resembling:
UserController
│
▼
UserService
│
▼
UserRepository
│
▼
Database
and construct the object graph automatically.
Containers may handle:
- dependency resolution
- component registration
- object creation
- configuration
- scopes
- lifetimes
- factories
- initialization
- disposal
- interception
- proxies
- decorators
- lazy initialization
IoC containers are essentially working with graphs.
Suppose:
OrderController
│
▼
OrderService
┌──┴──────┐
▼ ▼
Inventory PaymentService
Repository │
│ ▼
▼ PaymentGateway
Database
The container determines the construction order.
Conceptually:
Database
↓
InventoryRepository
PaymentGateway
↓
PaymentService
InventoryRepository + PaymentService
↓
OrderService
↓
OrderController
This becomes valuable as applications grow to hundreds or thousands of components.
There are several common styles.
class Service {
Service(Repository repository) {
this.repository = repository;
}
}Usually the preferred approach because dependencies are explicit and can often be immutable.
service.setRepository(repository);Useful for optional or reconfigurable dependencies, although it allows temporarily invalid objects if used carelessly.
For example:
@Inject
Repository repository;Convenient but hides dependencies from the constructor/API and can make isolated testing or reasoning harder.
A dependency is supplied only for a particular operation:
void process(Logger logger) {
}Useful when the dependency belongs to the operation rather than the object's entire lifetime.
IoC occurs in many systems where DI isn't the central mechanism.
button.onclick = () => {
console.log("clicked");
};The browser controls when your function runs.
You define:
onClick()
onPaint()
onClose()
onKeyPress()
The GUI framework invokes them.
You define:
@app.get("/hello")
def hello():
return "Hello"The framework decides when the handler executes.
You write:
@Test
void shouldCreateUser() {
}The test runner discovers and invokes it.
A host application loads:
Plugin A
Plugin B
Plugin C
and calls predefined plugin interfaces.
An OS invokes:
- interrupt handlers
- signal handlers
- callbacks
- event handlers
Developers implement hooks resembling:
start()
update()
render()
collision()
The engine controls the main loop.
Consider:
abstract class DataProcessor {
public void process() {
read();
transform();
save();
}
protected abstract void transform();
}A subclass implements:
class CsvProcessor extends DataProcessor {
protected void transform() {
...
}
}Notice the direction:
Superclass/framework algorithm
│
│ calls
▼
Subclass implementation
The parent defines the algorithm while subclasses fill in selected operations.
This is Template Method, and it embodies IoC.
IoC is much older than modern DI frameworks.
Its intellectual roots come from several areas of computing.
Operating systems and event-driven environments already used inversion-like mechanisms: programs supplied routines that systems invoked when events occurred.
The concept therefore predates the terminology commonly used today.
Object-oriented frameworks increasingly used patterns where:
framework owns algorithm
application supplies specialization
This became fundamental to GUI frameworks and application frameworks.
Framework architectures made IoC increasingly visible.
Instead of applications controlling every operation, frameworks supplied:
main loop
lifecycle
extension points
callbacks
base classes
Application developers filled in the missing behavior.
A historically important paper is "Designing Reusable Classes" by Ralph Johnson and Brian Foote.
It discusses reusable object-oriented frameworks and the characteristic reversal where framework code calls application-specific code.
The ideas helped formalize what became known as inversion of control.
The expression:
Don't call us, we'll call you.
became a memorable explanation for framework-based inversion.
The rise of object-oriented design patterns strengthened architectural approaches involving callbacks, factories, strategies, observers, template methods, and abstract interfaces.
The famous 1994 Design Patterns book by Gamma, Helm, Johnson, and Vlissides—the "Gang of Four"—helped popularize many mechanisms closely related to IoC.
Enterprise Java technologies increasingly relied on containers to manage:
transactions
security
persistence
lifecycles
components
configuration
Instead of application code managing infrastructure directly, containers took responsibility.
Projects such as Spring popularized a less heavyweight approach based heavily on dependency injection and plain objects.
Inversion of Control Containers and the Dependency Injection pattern
Martin Fowler's influential 2004 article "Inversion of Control Containers and the Dependency Injection pattern" helped establish Dependency Injection as the preferred term for the specific pattern of externally supplying object dependencies.
That terminology was useful because "Inversion of Control" had become too broad:
What control is being inverted?
"Dependency Injection" identifies the specific mechanism.
These are also commonly confused.
They are related but different.
Concerns who controls creation, execution, lifecycle, or orchestration.
The D in SOLID.
Roughly:
High-level policy should not depend directly on low-level implementation details; both should depend on abstractions.
Consider:
Bad:
OrderService
↓
MySQLRepository
DIP encourages:
OrderService
↓
OrderRepository
▲
│ implements
MySQLRepository
DI can then wire them:
Container
│
├── MySQLRepository
│
└── inject into OrderService
So:
DIP = architectural dependency direction
DI = mechanism for providing dependencies
IoC = broader transfer/inversion of control
They frequently work together but aren't synonyms.
Another approach is:
class OrderService {
void process() {
PaymentGateway gateway =
ServiceLocator.get(PaymentGateway.class);
}
}Control has moved away from direct construction, but the service still actively asks for its dependency.
With DI:
class OrderService {
OrderService(PaymentGateway gateway) {
this.gateway = gateway;
}
}The dependency comes to the service.
Compare:
Service Locator:
Component ── asks ──► Locator ── returns ──► Dependency
versus:
Dependency Injection:
Container ── provides ──► Component
+
Dependency
DI is often preferred because dependencies are visible in the component's API.
A particularly important concept in DI architectures is the Composition Root.
It is the location where the application's object graph is assembled.
For example:
public static void main(String[] args) {
Database db = new PostgresDatabase();
UserRepository repository =
new SqlUserRepository(db);
EmailSender email =
new SmtpEmailSender();
UserService service =
new UserService(repository, email);
Application app =
new Application(service);
app.run();
}Notice that most classes don't know how their dependencies are constructed.
Only the composition root does.
Composition Root
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Database Repository Email
│
▼
Service
│
▼
Application
Keeping composition centralized can give you many IoC/DI benefits without using a container at all.
Spring Framework is perhaps the most famous example of an IoC container in mainstream enterprise development.
A simplified Spring example:
@Repository
class SqlUserRepository
implements UserRepository {
}
@Service
class UserService {
private final UserRepository repository;
UserService(UserRepository repository) {
this.repository = repository;
}
}Spring discovers/manages components and resolves:
UserService requires UserRepository
↓
find implementation
↓
SqlUserRepository
↓
construct dependency
↓
construct UserService
Spring calls managed objects beans, and its container handles their creation and lifecycle.
Microsoft .NET has dependency injection deeply integrated into its modern application stack.
Conceptually:
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<UserService>();Then:
public class UserService
{
private readonly IUserRepository repository;
public UserService(IUserRepository repository)
{
this.repository = repository;
}
}The runtime/framework resolves the graph.
IoC appears even without traditional DI containers.
Consider a UI framework:
function Button() {
return <button onClick={handleClick}>Click</button>;
}The application doesn't directly control:
DOM event loop
render scheduling
event dispatch
component lifecycle
The framework/browser controls much of this.
Likewise hooks such as:
useEffect(() => {
...
}, []);register behavior that the framework decides when to execute.
So frontend frameworks are full of control inversion even where developers rarely use the phrase "IoC."
Consider:
server.on("request", (req, res) => {
...
});Your application registers a callback.
The runtime controls:
wait for network event
↓
receive request
↓
invoke callback
Again:
Don't call us; we'll call you.
Frameworks such as web servers and testing systems demonstrate the same principle.
For example:
def test_addition():
assert 1 + 1 == 2You don't necessarily write:
test_addition()The test runner discovers and invokes it.
Likewise web frameworks invoke route handlers.
Mobile platforms heavily invert control.
Developers provide lifecycle hooks resembling:
onCreate()
onStart()
onResume()
onPause()
onStop()
onDestroy()
Your application doesn't decide when the operating system pauses your activity.
The platform does.
Android/system lifecycle
│
▼
onCreate
│
▼
onStart
│
▼
onResume
The framework owns lifecycle control.
Game engines often expose functions resembling:
void Start() {
}
void Update() {
}The engine runs:
while game_running:
process_input()
update_physics()
call_user_Update()
render()
Developers provide behavior; the engine owns the execution loop.
This is a textbook IoC architecture.
Testing frameworks provide another clean example.
You write:
@BeforeEach
void setup() {}
@Test
void createsUser() {}
@AfterEach
void cleanup() {}The test framework determines:
discover tests
↓
instantiate test class
↓
setup()
↓
test()
↓
cleanup()
Your code supplies extension points inside the framework's execution algorithm.
Suppose an editor supports extensions.
The host defines:
interface Plugin {
void initialize();
void execute();
}Plugins implement:
class MyPlugin implements Plugin {
...
}The application performs:
discover plugins
↓
load plugins
↓
initialize plugins
↓
invoke plugin functionality
Plugins don't control the host.
The host controls plugins.
This architecture enables applications such as IDEs, browsers, editors, build systems, and game engines to be extensible.
Consider a message consumer:
@MessageListener("orders")
void handle(OrderCreated event) {
...
}The application doesn't necessarily decide:
now call handle()
Instead:
message broker
↓
runtime
↓
dispatcher
↓
handler
Event-driven architecture therefore frequently contains IoC at multiple levels.
A useful conceptual distinction is:
Your code asks:
Do you have work?
Do you have work?
Do you have work?
Example:
while True:
message = queue.get()
process(message)You register:
on_message(process)Then:
runtime receives message
↓
runtime invokes process()
The latter demonstrates execution control being moved outward.
IoC containers frequently manage object lifetimes.
Typical scopes include:
Singleton
One instance for application/container
Transient
New instance each resolution
Request
One instance per HTTP request
Session
One instance per user/session
Thread
One instance per thread
Example:
HTTP Request #1
├── Controller A
├── Service A
└── RequestContext #1
HTTP Request #2
├── Controller B
├── Service B
└── RequestContext #2
The application components don't need to manually create and destroy each request-scoped object.
IoC often externalizes configuration as well.
Instead of:
Database db =
new Database(
"localhost",
5432,
"production"
);the component receives configuration:
Database(DatabaseConfig config)and the environment/container supplies:
environment variables
configuration files
secret manager
command-line options
│
▼
configuration
│
▼
container
│
▼
Database
This separates configuration from behavior.
Factories can participate in IoC.
Instead of:
new PdfExporter()you might use:
Exporter exporter =
exporterFactory.create(format);The component delegates construction decisions.
A container may itself rely heavily on factories internally.
Suppose:
class PriceCalculator {
private PricingStrategy strategy;
PriceCalculator(PricingStrategy strategy) {
this.strategy = strategy;
}
}Different strategies:
RegularPricing
HolidayPricing
PremiumPricing
External code selects the strategy.
configuration
│
▼
PricingStrategy
│
▼
PriceCalculator
The calculator doesn't determine which algorithm it uses.
That represents dependency-level control inversion.
Observer/event systems naturally invert execution control:
Publisher
│
├── notify Observer A
├── notify Observer B
└── notify Observer C
Observers register behavior and wait to be called.
This idea appears everywhere:
DOM events
message buses
GUI systems
reactive programming
domain events
webhooks
event emitters
The simplest form of IoC may simply be passing a function:
fetchData(result => {
console.log(result);
});You provide:
what should happen
while something else controls:
when it happens
That separation is fundamental to asynchronous programming.
Consider:
items.map(transform)You provide transform.
The map implementation determines:
how iteration occurs
when transform runs
how results are collected
At a small scale, this contains the same structural idea:
algorithm owns control
│
▼
calls supplied behavior
IoC therefore isn't restricted to giant enterprise frameworks.
Web frameworks often use middleware:
Request
↓
Authentication
↓
Logging
↓
Authorization
↓
Controller
↓
Response
You implement middleware components.
The framework determines:
when they execute
what request reaches them
how errors propagate
how the chain is assembled
Again, control resides primarily in infrastructure.
Containers can intercept operations:
Controller
↓
Service proxy
↓
┌───────────────┐
│ transaction │
│ logging │
│ authorization │
│ metrics │
└───────────────┘
↓
actual service
The service might contain:
void transferMoney(...) {
...
}while infrastructure handles:
start transaction
call service
commit transaction
or:
start transaction
call service
exception
rollback
This removes certain cross-cutting concerns from business code.
The main benefits come from separating policy/behavior from orchestration and infrastructure.
A well-designed component can say:
"Here's what I need and what I do."
instead of:
"Here's what I need, how to construct it, where to find it, how to configure it, when to start it, and how to dispose of it."
That separation can dramatically improve architecture.
Without IoC:
class ReportService {
MySqlDatabase database =
new MySqlDatabase();
SmtpMailer mailer =
new SmtpMailer();
}ReportService knows specific technologies.
With abstractions:
class ReportService {
ReportService(
Database database,
Mailer mailer
) {}
}It only knows the capabilities it requires.
Consider:
class CheckoutService {
CheckoutService(PaymentGateway gateway) {
this.gateway = gateway;
}
}Production:
CheckoutService
↓
StripeGateway
Test:
CheckoutService
↓
FakePaymentGateway
Test:
var fakeGateway =
new FakePaymentGateway();
var service =
new CheckoutService(fakeGateway);No real payment API is required.
This is one of DI's most practical advantages.
Suppose your application depends on:
interface Storage {
void save(File file);
}Possible implementations:
LocalStorage
S3Storage
AzureBlobStorage
MemoryStorage
Business logic depends on:
Storage
rather than any particular vendor.
Composition determines the actual implementation.
Containers can manage expensive resources:
database pools
HTTP clients
thread pools
caches
message-broker connections
Rather than each component constructing its own resources.
A service should ideally concentrate on business behavior:
OrderService:
✓ validate order
✓ calculate order
✓ submit order
not necessarily:
✗ parse config
✗ create DB pool
✗ configure HTTP client
✗ discover logger
✗ initialize metrics
IoC helps move infrastructure responsibilities toward the application's edges.
Frameworks can define stable extension points:
Framework
│
├── AuthenticationProvider
├── StorageProvider
├── Renderer
└── Plugin
Applications supply implementations.
The framework doesn't need to know every future implementation.
IoC isn't automatically good.
It introduces architectural trade-offs.
A major one is indirection.
Compare:
var service =
new UserService(
new SqlRepository()
);with a large container-managed system where you see:
UserService service;and wonder:
Where did this object come from?
The answer might involve:
annotations
component scanning
configuration
container registration
profiles
factories
proxies
conditional bindings
Understanding runtime behavior can become harder.
Poorly designed IoC systems can feel magical:
I declared an interface.
Somehow an object appeared.
Which implementation?
Why?
When was it created?
Is it singleton?
Why is it proxied?
Why did this annotation change it?
The more implicit the wiring, the greater the cognitive cost.
This is why many developers favor explicit constructor injection even when using a container.
Manual construction may produce compile-time-visible problems.
A dynamically configured container may fail during startup:
Cannot resolve dependency:
PaymentGateway required by CheckoutService
or worse, during runtime.
Modern frameworks mitigate this through static analysis, generated wiring, startup validation, and compile-time DI.
47. Hidden dependency problems
Field injection:
@Inject
Logger logger;
@Inject
Repository repository;
@Inject
PaymentGateway payment;can hide the fact that the class requires three collaborators.
Constructor injection makes that explicit:
CheckoutService(
Logger logger,
Repository repository,
PaymentGateway payment
)If the constructor grows to:
Service(
A a,
B b,
C c,
D d,
E e,
F f,
G g,
H h
)that's valuable architectural feedback.
The class may have too many responsibilities.
A notorious IoC/DI mistake is creating interfaces for everything:
IUserService
UserService
IUserRepository
UserRepository
ILoggerService
LoggerService
IClockService
ClockService
An abstraction is useful when it represents a meaningful boundary, variation point, test seam, or architectural contract.
Creating:
interface Foo {
}solely because a DI container exists usually adds ceremony rather than flexibility.
You can inject concrete classes:
Service(MySqlRepository repository)That's still DI.
Conversely, using an interface doesn't automatically produce good architecture.
Good decoupling depends on where abstractions are placed and what architectural decisions they isolate.
DI is sometimes explained as:
"It's for mocking."
That's too narrow.
Testing is one benefit.
DI's more fundamental purpose is:
Separating object use from object construction/composition.
That enables:
testing
configuration
implementation substitution
lifecycle management
modularity
deployment variation
Mocking is only one application.
It's useful to think of IoC as a spectrum.
Function level
callback
Object level
injected dependency
Algorithm level
template method
Application level
framework
Runtime level
event loop
System level
orchestrator/platform
The same underlying idea repeats at different scales.
A traditional standalone program:
main()
│
├── call A
├── call B
├── call C
└── exit
A framework application:
Framework main loop
│
├── call application hook
│
├── call application handler
│
├── call application callback
│
└── repeat
The application becomes a participant inside a larger execution environment.
IoC often accompanies declarative systems.
Instead of:
Create server.
Open socket.
Listen.
Parse request.
Route request.
Serialize response.
you declare:
@app.get("/users")
def users():
...You describe:
"When a GET request arrives here, use this."
The framework handles the procedural machinery.
Similarly:
HTML
CSS
SQL
Kubernetes manifests
build configurations
CI workflows
can involve declarative relationships where another system controls execution.
Not every declarative system should automatically be labeled IoC, but the concepts frequently overlap.
IoC becomes particularly important at architectural boundaries.
Suppose:
Application Core
OrderService
│
▼
PaymentPort
▲
│
┌──────────┴──────────┐
│ │
StripeAdapter FakeAdapter
The business layer defines what capability it needs.
Infrastructure supplies the implementation.
At runtime:
infrastructure → injects → application
while source-code dependency can point toward abstractions owned by the application.
This is central to architectures described as:
- Hexagonal Architecture
- Ports and Adapters
- Clean Architecture
- Onion Architecture
Inside a microservice:
HTTP Controller
↓
Application Service
↓
Domain
↓
Ports
↑
│
Adapters
IoC is commonly used to wire:
database adapters
message publishers
external APIs
caches
authentication
metrics
For example:
PaymentService
↓
PaymentGateway interface
↑
│
StripeAdapter
The application core doesn't need to know how the vendor SDK is constructed.
Serverless computing is an extreme example of lifecycle inversion.
You write:
exports.handler = async event => {
...
};You don't control:
process startup
server provisioning
request routing
instance scheduling
shutdown
The platform invokes your handler.
Conceptually:
Cloud platform
↓
event
↓
your function
↓
result
That's very much an IoC-style execution model.
At a broader systems level, orchestration can resemble control inversion.
You declare desired state:
I want 3 instances of this service.
An orchestrator determines:
where they run
when they restart
how replacement occurs
This isn't the classic object-oriented definition of IoC, so calling Kubernetes an "IoC container" would be misleading.
But the broader architectural idea—declaring behavior/state while an external system owns orchestration—is strongly analogous.
Reactive systems frequently reverse traditional control.
Instead of:
ask repeatedly for new data
you subscribe:
stream.subscribe(value => {
...
});Then:
source emits
↓
runtime propagates
↓
subscriber invoked
The stream controls when data reaches your component.
A useful generalization is:
Traditional:
Consumer → Producer
"Give me something now."
versus:
Inverted:
Consumer → registers interest
Producer → later invokes consumer
This pattern appears in:
callbacks
events
observers
subscriptions
reactive streams
message consumers
webhooks
Event-driven architecture often uses IoC, but they're not synonymous.
Event-driven architecture describes systems organized around events.
IoC describes control being delegated/inverted.
You can have IoC without events:
constructor injection
template methods
and event-driven architectures usually contain substantial IoC.
A callback is a mechanism.
IoC is the design relationship.
For example:
setTimeout(doSomething, 1000);The callback is:
doSomething
The inversion is:
timer/runtime decides when doSomething executes
The cleanest cheat sheet is:
| Concept | Main question |
|---|---|
| IoC | Who controls execution/composition? |
| DI | Who supplies dependencies? |
| DIP | What direction should source dependencies point? |
| Service Locator | Where does a component obtain dependencies? |
| Factory | Who constructs an object? |
| Callback | Who invokes supplied behavior later? |
Imagine an online store.
Without IoC:
class CheckoutService {
private MySqlOrderRepository repo =
new MySqlOrderRepository();
private StripeGateway stripe =
new StripeGateway();
private SmtpEmailSender email =
new SmtpEmailSender();
void checkout(Order order) {
stripe.charge(order.total());
repo.save(order);
email.sendReceipt(order);
}
}The class is responsible for both:
business logic
+
infrastructure selection/construction
Now introduce boundaries:
class CheckoutService {
private final OrderRepository repo;
private final PaymentGateway payment;
private final EmailSender email;
CheckoutService(
OrderRepository repo,
PaymentGateway payment,
EmailSender email
) {
this.repo = repo;
this.payment = payment;
this.email = email;
}
void checkout(Order order) {
payment.charge(order.total());
repo.save(order);
email.sendReceipt(order);
}
}Production wiring:
CheckoutService
│
├── SqlOrderRepository
├── StripePaymentGateway
└── SmtpEmailSender
Tests:
CheckoutService
│
├── InMemoryOrderRepository
├── FakePaymentGateway
└── FakeEmailSender
The business class doesn't change.
That is the practical power of dependency-level IoC.
IoC/DI becomes particularly useful when:
- components have meaningful external dependencies;
- implementations vary by environment;
- infrastructure should be separated from domain logic;
- components need isolated tests;
- object lifetimes require management;
- plugins or extension points are needed;
- a framework controls application lifecycle;
- cross-cutting infrastructure needs centralized management;
- the dependency graph is large enough that centralized composition helps.
Consider a tiny program:
parser = Parser()
formatter = Formatter()
app = App(parser, formatter)
app.run()Adding:
container registrations
reflection
annotations
automatic scanning
lifecycle configuration
may make it worse.
IoC is a principle.
An IoC container is optional infrastructure.
Manual DI can often be simpler.
Many applications can evolve like this:
Stage 1
class A:
b = B()
Then:
Stage 2
class A:
def __init__(self, b):
self.b = b
Then composition:
Stage 3
b = B()
a = A(b)
Only when complexity justifies it:
Stage 4
container.register(B)
container.register(A)
a = container.resolve(A)
This avoids introducing container complexity before it's actually useful.
Several problems show up repeatedly.
Service Locator everywhere: classes pull arbitrary dependencies from a global container, making their true requirements invisible.
Container leaking into domain code: business logic starts depending on container APIs.
Excessive interfaces: every class gets an interface despite there being no useful abstraction.
Field injection everywhere: dependencies become hidden and objects can exist in partially initialized states.
Huge dependency graphs: DI makes architectural coupling easier to see, but doesn't automatically fix it.
Automatic scanning everywhere: discovering where a dependency came from becomes difficult.
Container as global state: the IoC container becomes a sophisticated global-variable registry.
Mocking everything: tests reproduce implementation structure rather than testing meaningful behavior.
For dependency-oriented IoC, a strong default is:
1. Prefer constructor injection.
2. Keep dependencies explicit.
3. Keep the container near the application boundary.
4. Keep domain/business code unaware of the container.
5. Use meaningful abstractions, not interfaces mechanically.
6. Prefer manual composition when the graph is simple.
7. Validate the dependency graph at startup.
8. Understand component lifetimes.
9. Avoid injecting enormous numbers of dependencies.
10. Treat IoC as architecture—not an annotation trick.
IoC is ultimately about separating two questions:
from:
For example:
Business component:
"If asked to process an order,
here is how order processing works."
Infrastructure:
"I'll determine when you're instantiated,
what implementations you receive,
when requests reach you,
and when you're destroyed."
That division allows business logic to become more independent of its execution environment.
Another useful perspective is:
Policy
"What should the application do?"
separated from
Mechanism
"How is execution/resources/infrastructure managed?"
A framework or container often manages mechanisms while application code supplies policy.
That idea goes well beyond object construction.
Imagine two programmers.
Your component says:
I need a database.
I'll construct PostgreSQL.
I need payments.
I'll construct Stripe.
I need email.
I'll configure SMTP.
Now I'll perform my business logic.
Your component says:
To do my job I require:
- an OrderRepository
- a PaymentGateway
- an EmailSender
Give me implementations and I'll perform checkout.
Another part of the system answers:
For production:
OrderRepository → PostgreSQL
PaymentGateway → Stripe
EmailSender → SMTP
For tests:
OrderRepository → Memory
PaymentGateway → Fake
EmailSender → Fake
The component knows what capabilities it requires, but not necessarily how those capabilities are constructed or selected.
That's the essence.
INVERSION OF CONTROL
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
Construction Execution Lifecycle
Control Control Control
│ │ │
▼ ▼ ▼
Dependency Callbacks Framework/
Injection Events Container
│ │
┌──────┼──────┐ ├── GUI
▼ ▼ ▼ ├── Web
Constructor Setter Field ├── Tests
├── Games
└── Reactive
And related—but distinct—ideas surround it:
IoC
│
┌───────┴────────┐
│ │
▼ ▼
DI Callbacks
│
▼
Composition
│
├──── may use ────► IoC Container
│
└──── may be ─────► Manual
Related architectural principle:
Dependency Inversion Principle
│
▼
depend on abstractions
If you remember only one thing, make it this:
Inversion of Control means moving control over some aspect of a program—such as execution, dependency creation, dependency selection, or lifecycle—from the component itself to an external mechanism.
And if you remember three distinctions:
IoC = the broad principle.
DI = externally supplying a component's dependencies.
IoC container = infrastructure that can automate dependency composition and lifecycle.
The reason IoC matters isn't that new is inherently bad or that every class needs an interface. Its value is separating components from decisions they shouldn't need to own. At its best, your business code describes its capabilities and requirements, while frameworks, composition roots, runtimes, and infrastructure handle the machinery around it.
