ARCHITECTURAL

Dependency Injection

What is it?

Dependency Injection is a technique where a class receives the objects it needs from the outside instead of creating them internally. In other words, the class does not create its own dependency with something like new HealthService(). Instead, it works with an object provided to it through an interface such as IHealthService.

The main idea is simple: a class should not need to know how the system it uses is created. It should only use the dependency given to it. This keeps systems such as PlayerController, AudioManager, SaveSystem, and InputService less tightly connected.

This idea is closely related to Inversion of Control. The responsibility of creating and connecting objects is moved outside the class. Dependency Injection is one of the most common ways to apply this principle.

When is it used?

Dependency Injection is useful when a system may need to change later. For example, you may use LocalSaveSystem today and replace it with CloudSaveSystem later. With DI, you do not need to modify PlayerController; you only change the dependency being provided.

It is also very useful when writing tests. Instead of using a real AudioManager or HealthService, you can provide a fake version such as MockAudioManager or FakeHealthService. This makes it easier to test classes without setting up a full Unity scene or running real services.

DI becomes more valuable as a project grows. When systems such as AudioManager, UIManager, SaveSystem, and InputService start depending on each other, creating them directly inside each class can quickly become messy. Managing dependencies from a central place keeps the project cleaner.

For very small prototypes, simple data classes, DTOs, structs, or one-file experiments, DI may be unnecessary. In those cases, adding too much abstraction can make the project more complicated than it needs to be.

Animation

classPlayer
IHealthServicehealthService= HealthService@ref
voidTakeDamage(int)
classHealthServiceimplements IHealthService
voidReduce(int amount)

Code

Assets / Scripts / Architectural / DependencyInjectionPlayer.cs
READ ONLY
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace Patterns.Architectural.DependencyInjection
{
    public class Player
    {
        private readonly IHealthService _healthService;

        public Player(IHealthService healthService)
        {
            _healthService = healthService;
        }

        public void TakeDamage(int amount)
        {
            _healthService.Reduce(amount);
        }
    }
}

Advantages and Disadvantages

• Advantages:

  • Loose coupling makes systems easier to replace or update.
  • Testability improves because fake or mock objects can be injected.
  • Readability improves because dependencies are clearly visible.

• Disadvantages:

  • It can add extra complexity, especially in small projects.
  • Debugging can be harder when dependencies are wired incorrectly.
  • Using a DI framework can make the project dependent on that tool.

Tips

In Unity, classic constructor injection is not always easy because MonoBehaviourobjects are created by Unity's scene system. Because of this, DI in Unity is usually done in three common ways: using [SerializeField] references from the Inspector, passing dependencies manually through an Init(...) method, or using frameworks like Zenject or VContainer.

For small and medium-sized projects, it is usually better to start with manual DI before adding a framework. A Bootstrap or GameInstaller object can create the services once and pass them to the scripts that need them.

Avoid searching for dependencies with FindObjectOfType, GameObject.Find, or repeated GetComponent calls. These approaches make your classes depend too much on the scene structure. It is cleaner to provide dependencies from the beginning.

Try to depend on interfaces instead of concrete classes. For example, use ISaveService instead of directly depending on SaveService. This makes it much easier to replace LocalSaveService with CloudSaveService later.

ScriptableObjects can also work as a lightweight alternative to DI in Unity. Shared data or service references can be placed inside a ScriptableObject and assigned through the Inspector. This can reduce the need for hard references and Singletons.

Singletons are not always forbidden, but they should be used carefully. GameManager.Instance may look convenient at first, but over time it can create a codebase where everything depends on everything else. The real value of DI is that dependencies stay clear, visible, and manageable.