Why Dependency Inversion Is an Ancient Idea
When Drupal adopted Symfony’s service container, many developers experienced it as a paradigm shift — a new way of thinking about how software components relate to one another. But note the principle at the heart of this shift:
depend on abstractions, not on concretions
This is not a new idea at all. It is one of the oldest and most repeatedly discovered insights in human thought.
Across philosophy, biology, linguistics, economics, and architecture, thinkers working independently and centuries apart kept arriving at the same conclusion:
systems that bind themselves to particulars become brittle; systems that bind themselves to principles endure.
The software engineer calls this Dependency Inversion. Plato called it the Theory of Forms. Adam Smith saw it in the division of labor. Saussure found it in the structure of language itself.
This article is about Drupal and Symfony’s dependency injection container — how it works, why it matters, and how to use it well. But before we get to the code, it is worth pausing to recognize that what we are really talking about is something much older than PHP. Understanding why this pattern keeps appearing across such different domains makes the technical details not just easier to learn, but harder to forget. See the appendix for similar concepts across different disciplines.
So, what are the reasons we use services for classes in Drupal
In Drupal, services (managed via the Symfony Dependency Injection container) are the preferred way to instantiate and use classes for several important reasons:
1. Dependency Injection (DI) Rather than hardcoding dependencies inside a class with new SomeClass(), services allow dependencies to be injected from the outside. This makes classes loosely coupled and easier to swap out or mock.
2. Single Instance / Reusability Services are typically registered as singletons. The container instantiates the class once and reuses it, avoiding redundant object creation and saving memory.
3. Testability Because dependencies are injected rather than created internally, you can easily substitute mock or stub implementations during unit testing without bootstrapping the full Drupal stack.
4. Discoverability & Consistency Registering a class as a service in mymodule.services.yml makes it part of a well-known, consistent system. Other developers (and Drupal itself) can find and use your class via \Drupal::service('mymodule.my_service') or constructor injection.
5. Decoupling from Global State Using \Drupal::someStaticMethod() ties code to Drupal's global state, making it harder to test and reuse. Services replace these static calls with injected objects, following modern OOP best practices.
6. Replaceability & Extensibility Any module can override or decorate a service via ServiceProvider classes or services.yml aliases. This is how Drupal's override system works — e.g., swapping the cache backend or replacing a core service entirely without patching core.
7. Lifecycle Management The container can manage object lifecycle — lazy instantiation, scoped services, synthetic services — giving you fine-grained control over when and how objects are created.
8. Adherence to Drupal's Architecture Core Drupal APIs (routing, entity systems, plugin managers, etc.) are all services. Building your own classes as services means they integrate naturally with Drupal's architecture and can consume core services cleanly.
In short: services enforce the Dependency Inversion Principle, make code testable and replaceable, and plug your classes into Drupal's standardized, container-managed ecosystem rather than relying on brittle static calls or manual instantiation.
JimBob, my co-worker says:
FYI... you don't really need a service unless you want to use \Drupal::service(). I just use the static syntax like MyEntity::build()
He does everything his own way it seems, and doesn't think services are needed. He calls it "bloat".
JimBob's not entirely wrong that static methods work — but he's missing the bigger picture, and the approach has real costs:
What JimBob is getting wrong:
Testability goes out the window Static calls like MyFooBuilder::build() cannot be mocked or swapped in unit tests. You're stuck testing the real implementation every time, or not testing at all. With a service, you inject an interface and swap in a mock trivially.
He's hiding dependencies When a class calls static methods internally, those dependencies are invisible — they don't show up in the constructor, there's no contract, and you can't tell what a class truly needs just by looking at it. This makes code harder to understand and maintain.
No replaceability If another module (or a future you) needs to swap out MyFooBuilder for a different implementation, a static call makes that nearly impossible. A service can be overridden cleanly via a ServiceProvider.
It often leads to tight coupling to global state Static methods frequently reach out to \Drupal:: calls or other globals internally, which compounds the testability and decoupling problems.
It doesn't scale well on a team It's a pattern that works fine in isolation but creates friction as the codebase grows — others won't know to look for static utility classes, and there's no central registry (the service container) to discover them.
The fair nuance: For truly stateless, pure utility functions with no dependencies — think a string formatter or a math helper — a static method is sometimes acceptable. But that's a narrow exception, not a general strategy.
JimBob has found something that works, but Drupal's service container exists precisely to avoid the problems his approach quietly accumulates over time. It's a classic case of optimizing for "writing code fast now" at the expense of "maintaining code later."
Sure! These are two closely related but distinct concepts that sit at the heart of modern software design.
Inversion of Control (IoC)
IoC is the broader principle. In traditional procedural thinking, your code is in charge — it decides what it needs and goes and gets it:
class MyService {
public function doSomething() {
$db = new DatabaseConnection(); // I'm in control, I create what I need
$db->query(...);
}
}
Inversion of Control flips this. Instead of your class reaching out to get its dependencies, something external (a framework, a container) is in control of providing them. Your class just declares what it needs and trusts that it will be provided.
The famous Hollywood principle sums it up: "Don't call us, we'll call you."
Drupal's service container is an IoC container — it's the thing in charge of wiring everything together.
Dependency Injection (DI)
DI is the most common way to implement IoC. Rather than a class creating or fetching its own dependencies, they are passed in — typically through the constructor:
class MyService {
public function __construct(
private DatabaseConnection $db // Injected from outside
) {}
public function doSomething() {
$this->db->query(...);
}
}
Now MyService doesn't know or care where DatabaseConnection came from. It just uses it. The container built it, wired it up, and handed it over.
The key insight: the class depends on an abstraction (ideally an interface), not a concrete implementation. This is the Dependency Inversion Principle — high-level code shouldn't depend on low-level details.
Why JimBob's Approach Ignores Both
When JimBob writes MyFooBuilder::build(), several things happen:
The calling class is in control again It reaches out and grabs exactly what it wants, just like the pre-IoC style. The control has not been inverted — JimBob's class is the one calling the shots about what runs and when.
There is no dependency to inject Because there's no object being passed in, there's nothing to swap, mock, or override. The static call is essentially hardwired. The "dependency" is baked into the call site itself.
The class is coupled to a concrete implementation MyFooBuilder::build() is not an interface — it's a specific class. Anything that calls it is tightly bound to that exact implementation forever, with no seam to insert a different one.
The container never gets involved Drupal's IoC container only manages objects registered as services. Static methods live entirely outside of it, so all the benefits — lifecycle management, replaceability, discoverability — simply don't apply.
A Simple Way to Think About It
JIMBOB's Way | Service / DI Way | |
|---|---|---|
Who creates dependencies? | The class itself | The container |
Who is in control? | The calling code | The framework (IoC) |
Can you swap implementations? | No | Yes |
Can you mock in tests? | No | Yes |
Is the dependency visible? | No | Yes (constructor) |
JimBob's approach isn't malicious — it's just a habit from an older style of PHP where none of this infrastructure existed. But in a Drupal/Symfony context, you have an IoC container right there doing all this work for you. Not using it means manually recreating problems that were already solved.
Appendix: The Same Idea, Everywhere
A. Philosophy — Plato’s Theory of Forms (~380 BCE)
Plato argued that the visible world of particular things — this chair, that tree — are imperfect copies of perfect, eternal Forms. The Form of a chair is not any specific chair; it is the abstract principle of chairness that all chairs approximate. True knowledge, for Plato, meant grasping the Form, not cataloguing particulars.
In software terms: your module should depend on the interface (the Form), not on any specific implementation (the particular). When Robert Martin formulated the Dependency Inversion Principle in the 1990s, he was, philosophically speaking, a Platonist.
B. Architecture — Louis Sullivan’s “Form Follows Function” (1896)
The American architect Louis Sullivan argued that a building’s shape should be determined by its purpose, not by decorative convention. The function — the abstraction — should govern; the form — the particular expression — should follow and remain flexible.
Inversion of Control applies the same logic to software construction. Rather than a component reaching out to instantiate its own dependencies (letting the particular govern), a container injects them from outside (letting the function, the interface contract, govern). Control flows from principle, not from implementation detail.
Biology — Convergent Evolution (19th century onwards)
Nature has independently evolved the eye at least forty times across different lineages. Birds, bats, and insects all developed wings from completely unrelated anatomical origins. These are examples of convergent evolution — radically different implementations arriving at the same abstract solution because the solution fits the problem so well.
A well-designed interface in a DI container behaves the same way. The abstraction (vision, flight, logging, routing) is stable and reusable; the concretion is free to vary. Nature, it turns out, has been practicing interface segregation for hundreds of millions of years.
D. Linguistics — Saussure’s Structural Sign Theory (1906–1911)
Ferdinand de Saussure, the father of modern linguistics, distinguished between the signifier (the sound or written word) and the signified (the concept it points to). Crucially, this relationship is arbitrary — there is nothing inherently tree-like about the word “tree.” Any signifier can be substituted as long as it reliably points to the same signified within a given system.
This is dependency injection stated in linguistic terms. The interface is the signified — stable, conceptual, abstract. The implementation is the signifier — arbitrary, swappable, concrete. A Spanish speaker and an English speaker can depend on the same concept while using entirely different words. Two Drupal modules can depend on the same service interface while using entirely different implementations.
E. Economics — Adam Smith and the Division of Labor (1776)
In The Wealth of Nations, Adam Smith observed that a factory organized around roles rather than individuals becomes dramatically more productive and resilient. The factory depends on the pin-maker role, not on any specific pin-maker. When one worker leaves, the system continues because the contract (the role, the interface) remains intact.
This is exactly what a service container provides. Your application depends on a mailer role, not on a specific mailer class. Swapping implementations — as Smith’s factory swaps workers — requires no restructuring of the system that depends on them.
Military Strategy — Mission-Type Tactics / Auftragstaktik (19th century)
Prussian military doctrine developed the concept of Auftragstaktik — commanding by intent rather than by specific instruction. Officers were given the objective (the abstraction) and trusted to determine their own means (the implementation). This made armies dramatically more adaptive, because units could respond to changing conditions without waiting for orders that addressed every particular.
IoC is the software equivalent: the container specifies what is needed (the interface), and the concrete implementation decides how to fulfill it. High-level policy does not depend on low-level procedure — it depends on the contract between them.
G. Mathematics — Abstract Algebra and Category Theory (19th–20th century)
Mathematicians discovered that many seemingly different structures — numbers, matrices, functions — share the same underlying algebraic laws. Abstract algebra studies these laws in isolation, independent of any particular instantiation. Category theory takes this further, studying relationships and transformations in their most abstract form, deliberately suppressing implementation details.
This is the mathematical spirit of an interface: define the operations and their laws, say nothing about how they are carried out, and let any structure that satisfies those laws be used interchangeably. Haskell’s type classes make this connection explicit; Symfony’s service contracts do the same thing in PHP.
Theology — Apophatic (Negative) Theology (various traditions)
A recurring strand of religious thought — found in Christian mysticism, Jewish Kabbalah, and Sufi Islam — holds that God cannot be described in particular terms, only in abstract or negative ones. To say God is this specific thing is to diminish and misrepresent. The divine is approached through principles, not through particulars.
This is a more contemplative parallel, but the structural logic is identical: binding yourself to a particular description closes off possibilities; holding to the abstract keeps the system open. Whether the domain is theology or software, over-specification is considered a kind of error.
The pattern is consistent enough to suggest that Dependency Inversion is not a design preference or a stylistic convention. It is closer to a discovered truth about the nature of complex, adaptive systems — one that human beings keep finding, in different languages and different centuries, whenever they think carefully about how parts should relate to wholes.
Recent content
-
4 hours 48 minutes ago
-
5 hours ago
-
6 hours 49 minutes ago
-
20 hours 42 minutes ago
-
3 days ago
-
3 days ago
-
6 days 3 hours ago
-
6 days 23 hours ago
-
1 week 5 days ago
-
1 week 6 days ago