Dependency Injection
The mechanism that wires layers together - and the trap of using it without architecture.
What Dependency Injection Actually Is
Dependency Injection (DI) is a simple idea with a complicated reputation. At its core, it means: instead of a class creating or finding its own dependencies, those dependencies are provided from outside.
Without DI:
class QuestSystem {
private SaveService _saveService = new SaveService(); // creates its own dependency
private PlayerStats _stats = GameManager.Instance.Stats; // finds it via global access
}
With DI:
class QuestSystem {
private ISaveService _saveService; // provided from outside
private IPlayerStats _stats; // provided from outside
QuestSystem(ISaveService saveService, IPlayerStats stats) {
_saveService = saveService;
_stats = stats;
}
}
That’s it. The class declares what it needs (through a constructor, a field, or a method parameter), and something else provides it. The class never knows the concrete type - only the interface.
Why DI Matters for Game Architecture
DI is the primary mechanism that makes the Architecture Model work in practice. Without it, the dependency inversion principle is just theory.
Consider the Core layer. It defines an IPersistence interface. Something needs to connect that interface to a real implementation at runtime. DI does this wiring - the Core asks for IPersistence, and the DI system provides the configured implementation (local file, cloud save, in-memory test double) without the Core knowing or caring which one.
This enables:
- Layer independence - inner layers never reference outer layer types
- Testability - swap real implementations for test doubles by changing the DI configuration
- Flexibility - switch implementations (e.g., local save vs. cloud save) without changing any consuming code
Buy vs. Build
There are two approaches to DI in game projects:
Use an Existing Framework
Most languages and engines have DI frameworks available. Unity has VContainer, Zenject/Extenject, and others. Unreal has built-in dependency handling patterns. Godot has community solutions.
Advantages:
- Mature, tested, optimized
- Community support and documentation
- Features like lifecycle management, scoping, and automatic registration
Disadvantages:
- Another dependency in your project
- May impose conventions that don’t align with your architecture
- Can be overkill for what you actually need
- Learning curve for the team
Build a Minimal Solution
A DI container at its simplest is a dictionary that maps interfaces to implementations. You can build something sufficient in a few hundred lines of code.
Advantages:
- You understand every line
- Tailored exactly to your needs
- No external dependency
- Can enforce your specific architectural rules
Disadvantages:
- You own the maintenance
- Missing features you don’t know you need yet
- Risk of reinventing what exists
Our Recommendation
For teams starting out, using an existing framework is pragmatic - it lets you focus on architecture instead of infrastructure. As your understanding deepens, you may find that a custom solution better serves your specific needs.
The choice matters far less than the discipline of using DI correctly. A simple hand-rolled container used well beats a sophisticated framework used poorly.
The Singleton in Disguise
This is the most important section of this page.
DI is frequently presented as the cure for the Singleton Anti-Pattern. The logic goes: “Singletons are bad because they create global state. DI eliminates singletons. Therefore DI solves the problem.”
This is dangerously incomplete.
Consider a codebase where 40 classes all depend on GameManager.Instance:
// Before: Singleton
class QuestSystem {
void CompleteQuest() {
GameManager.Instance.Stats.AddXP(100);
GameManager.Instance.Audio.Play("quest_complete");
GameManager.Instance.UI.ShowNotification("Quest complete!");
GameManager.Instance.Analytics.Track("quest_completed");
GameManager.Instance.SaveSystem.Save();
}
}
Now “fix” it with DI:
// After: DI... but is it better?
class QuestSystem {
[Inject] private IGameManager _gameManager;
void CompleteQuest() {
_gameManager.Stats.AddXP(100);
_gameManager.Audio.Play("quest_complete");
_gameManager.UI.ShowNotification("Quest complete!");
_gameManager.Analytics.Track("quest_completed");
_gameManager.SaveSystem.Save();
}
}
Nothing has changed. The same 40 classes still depend on the same god-interface. The dependency graph is identical. The coupling is identical. The only difference is the mechanism of access - field injection instead of a static property.
This is a singleton in disguise. The DI container has become a glorified service locator, and IGameManager is still the center of the universe.
The Real Problem
The problem with singletons was never the static Instance property. It was:
- God interfaces - one type that exposes everything to everyone
- Universal coupling - every class depends on the same central hub
- No architectural direction - dependencies point in every direction
DI fixes none of these problems by itself. You can have a DI container with perfectly inverted dependencies - and still have 40 classes all resolving the same IGameManager.
The Real Solution
DI becomes genuinely useful only when combined with the Architecture Model:
-
Break god interfaces into small, focused contracts - instead of
IGameManager, haveIPlayerStats,IQuestLog,IAudioService,IAnalytics. Each class depends only on what it actually needs. -
Respect the layer rules - the Core only resolves Core-level interfaces. The Use Cases layer resolves Core interfaces. Only the outermost layer sees everything.
-
Minimize the dependency surface - if your class needs five injected dependencies, it probably has too many responsibilities. DI should reveal design problems, not hide them.
// Actually fixed: small interface, minimal dependencies, clear layer
class CompleteQuestUseCase {
[Inject] private IQuestLog _questLog; // Core interface
[Inject] private IPlayerStats _playerStats; // Core interface
void Execute(string questId) {
_questLog.MarkCompleted(questId);
_playerStats.AddXP(_questLog.GetReward(questId).XP);
// UI, audio, analytics react via events - not called directly
}
}
The quest completion use case knows about quest logic and player stats. It doesn’t know about UI, audio, or analytics. Those systems react to domain events. The dependency graph is minimal, directional, and clean.
Common DI Pitfalls in Games
Resolving Everything Everywhere
If any class can resolve any interface from the container, you have a service locator - not dependency injection. The container should enforce what each layer is allowed to resolve.
Lifecycle Mismanagement
Game objects have complex lifecycles - they’re created, activated, deactivated, destroyed. DI containers must integrate with these lifecycles. A dependency resolved in a scene that outlives the scene creates subtle bugs. A dependency that isn’t disposed creates resource leaks.
Over-Injection
If a class has 10 injected fields, DI is revealing a design problem: the class has too many responsibilities. Don’t solve this by creating a facade that bundles 10 dependencies into one - that’s just GameManager by another name. Solve it by splitting the class.
Container as Global State
A DI container that’s accessible from everywhere and holds everything is just a dictionary-shaped singleton. The container should be configured at the composition root (application startup) and should not be referenced directly by business logic. Classes should receive their dependencies - they should never ask the container for them.
Summary
DI is an essential tool for implementing clean architecture. It’s the mechanism that wires interfaces to implementations, enabling the dependency inversion that makes layered architecture possible.
But DI is only a tool. Without the architectural rules to guide it - small interfaces, directional dependencies, layer boundaries - it degenerates into a sophisticated service locator that provides global access to everything. The “singleton in disguise” trap is real and extremely common.
The syringe is not the medicine. The Architecture Model is the medicine. DI is just the delivery mechanism.