Practical

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:

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:

Disadvantages:

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:

Disadvantages:

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:

  1. God interfaces - one type that exposes everything to everyone
  2. Universal coupling - every class depends on the same central hub
  3. 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:

  1. Break god interfaces into small, focused contracts - instead of IGameManager, have IPlayerStats, IQuestLog, IAudioService, IAnalytics. Each class depends only on what it actually needs.

  2. Respect the layer rules - the Core only resolves Core-level interfaces. The Use Cases layer resolves Core interfaces. Only the outermost layer sees everything.

  3. 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.