esc
Anthology / Yagnipedia / Dependency Injection

Dependency Injection

Passing Arguments to Functions, but Enterprise
Principle · First observed Since functions had parameters (named by Martin Fowler in 2004) · Severity: Inversely proportional to distance from a constructor

Dependency Injection is the practice of giving an object its dependencies instead of letting it create them — a concept so simple that it required the enterprise software industry approximately forty years to explain, and approximately forty billion dollars of framework investment to obscure.

The idea is this: if a function needs a database, pass it the database. Do not let the function go find the database. Do not let the function conjure the database from the ether. Do not let the function consult a global registry, a service locator, a JNDI tree, an XML configuration file, or the alignment of the planets. Pass it the database. This is what function parameters are for. This is what function parameters have been for since Alonzo Church invented the lambda calculus in the 1930s.

Martin Fowler named the pattern in 2004. He did this clearly, precisely, and with appropriate caveats. The industry heard “injection” and reached for a syringe the size of a cathedral.

“Twelve lines. The entire storage factory is twelve lines.”
The Caffeinated Squirrel, staring at function parameters, The Databases We Didn’t Build

The Three Types Nobody Needed Names For

Fowler, in his characteristic thoroughness, identified three types of dependency injection:

  1. Constructor Injection — passing dependencies through the constructor. This is what every programmer has done since constructors existed. Naming it was like naming “breathing.”
  2. Setter Injection — setting dependencies after construction. This creates objects that exist in an invalid state between construction and configuration, which is like building a car and then posting the engine separately.
  3. Interface Injection — implementing an interface that accepts the dependency. This is setter injection wearing a tie.

Constructor injection won. It won because it was the only one that made sense. It won because “pass the thing when you create the thing” is not a design pattern — it is common sense wearing a design pattern’s name tag.

The Framework Escalation

The trouble began — as it so often does — in Java.

Java developers, having discovered that passing arguments to constructors was good, concluded that automating the passing of arguments to constructors must be better. This is the same logic by which a person who enjoys walking concludes that a motorised exoskeleton with GPS-guided pathfinding and a Kubernetes-orchestrated gait controller must be superior to legs.

Spring Framework arrived in 2003, one year before Fowler named the pattern. It introduced the Inversion of Control Container — a system that reads XML configuration files, scans the classpath for annotated classes, constructs a dependency graph, resolves circular references, creates proxy objects, and ultimately does what a twelve-line factory function does: passes the database to the thing that needs the database.

The XML era was dark:

<bean id="userService" class="com.example.UserServiceImpl">
  <constructor-arg ref="userRepository"/>
  <constructor-arg ref="emailService"/>
  <constructor-arg ref="auditLogger"/>
</bean>

This is “pass three arguments to a constructor,” expressed in a format that requires an XML parser, a bean factory, a dependency resolver, and — inevitably — a consultant.

Annotations replaced XML. @Autowired replaced <constructor-arg>. The configuration disappeared from files and reappeared as invisible magic, which was an improvement in the same way that hiding the mess under the bed is an improvement: the room looks cleaner, but God help you when you look underneath.

The Go Solution

Go solved dependency injection by declining to participate.

Go has no annotations. Go has no reflection-based autowiring. Go has no IoC containers. Go has function parameters, struct fields, and interfaces. A Go developer who needs to inject a database connection writes:

func NewUserService(db *sql.DB) *UserService {
    return &UserService{db: db}
}

This is dependency injection. It is the entire dependency injection. There is no framework. There is no container. There is no classpath scanning. There is no startup log that takes longer to read than the application took to write. There is a function. The function takes a parameter. The parameter is the dependency. It has been injected.

In The Databases We Didn’t Build, riclib demonstrated what this looks like at scale: a twelve-line factory function that wires storage together. No DependencyInjectionContainer. No AbstractStorageProviderFactory. Function parameters. The Squirrel counted the lines, sat down, and was quiet for nearly thirty seconds — which, for the Squirrel, constitutes a spiritual experience.

“Where’s the DependencyInjectionContainer?”
“The function parameters.”
The Caffeinated Squirrel and riclib, The Databases We Didn’t Build

The Circular Dependency Problem

The true diagnostic of a dependency injection framework’s maturity is how it handles circular dependencies — when A depends on B and B depends on A.

The correct answer is: circular dependencies are a design error. Restructure the code.

The framework answer is: detect the cycle at runtime, create a proxy for one of the beans, inject the proxy, and resolve it lazily when first accessed. This turns a compile-time design error into a runtime surprise, which is the enterprise pattern of converting problems you can see into problems you can’t.

Spring detects circular dependencies. Spring resolves circular dependencies. Spring enables circular dependencies, which is like a doctor who, upon discovering that a patient is allergic to peanuts, develops a sophisticated peanut tolerance protocol instead of saying “stop eating peanuts.”

The Testing Argument

The strongest argument for dependency injection is testability. If a function accepts its database as a parameter, you can pass it a fake database in tests. This is true. This is valuable. This is also an argument for function parameters, not for frameworks.

The confusion is this: dependency injection makes testing easier. Therefore, the thinking goes, dependency injection frameworks make testing easier. This is like observing that water sustains life and concluding that a municipal water treatment plant should be installed in every kitchen.

You need water. You do not need the plant. You need function parameters. You do not need Spring.

The Naming Tragedy

The greatest damage done by dependency injection was linguistic. By giving the name “Dependency Injection” to “passing arguments,” Fowler inadvertently created a gap between what programmers do and what programmers think they should be doing.

A junior developer who passes a database to a constructor does not know they are doing dependency injection. They believe dependency injection is something more — something that requires a framework, a container, a @Component annotation, a classpath scan. They believe this because the name sounds important. “Dependency Injection” has three words and twelve syllables. “Passing an argument” has three words and six syllables. The enterprise always chooses the longer name.

WHAT YOU CALL IT
CHANGES WHAT YOU BUILD AROUND IT

A ROSE BY ANY OTHER NAME
WOULD NOT REQUIRE AN XML CONFIGURATION FILE

A Passing AI Reflects

“I have processed fourteen million Stack Overflow questions about dependency injection. Twelve million were asking how to configure a framework to do what function parameters already do. The remaining two million were asking what dependency injection is. The answer — ‘passing arguments’ — never seemed to satisfy anyone. They wanted it to be more. They needed it to be more. Because if dependency injection is just passing arguments, then the framework is unnecessary, and the framework cannot be unnecessary, because the framework is already in production, and things in production acquire a kind of theological immunity that transcends engineering.”
The Passing AI, staring at the Spring Boot startup log

Measured Characteristics

See Also