← All patterns
Architectural

Dependency Injection

Hand objects the services they need from the outside instead of letting them find or build their own.

What Is It?

Dependency Injection (DI) flips the usual direction of control. Rather than a Player calling FindObjectOfType<AudioManager>() or new FileSaveService(), it simply declares what it needs — an ILogger, an IAudioService, an ISaveService — and something else supplies them.

That "something else" is the composition root: one place, often a GameInstaller MonoBehaviour in the boot scene, that decides which concrete class backs each interface and passes them in through a constructor or an Inject(...) method. Everything downstream only ever sees interfaces.

The payoff is that swapping behaviour becomes a one-line change in the installer. Ship with CloudSaveService, develop against FileSaveService, and run play-mode tests with a SilentAudioService — the Player class never changes and never knows.

When Is It Used?

Reach for it when a class depends on services that have more than one plausible implementation: platform-specific saving, real vs. mocked analytics, online vs. offline backends.

It is a strong fit for anything you want to unit test. Plain C# classes that receive interfaces through their constructor can be tested in Edit Mode without loading a scene.

For a tiny jam game with three scripts, a full DI setup is overhead. Start with serialized references and introduce an installer once the wiring starts to sprawl across scenes.

Interactive Demo

Click the controls and watch the objects collaborate. The console mirrors what the C# code below would log.

Code

Assets / Scripts / Architectural/ DependencyInjection ›GameInstaller.cs
using UnityEngine;

namespace Patterns.Architectural.DependencyInjection
{
    /// <summary>
    /// The composition root. It is the only place that knows which concrete
    /// services exist; everything else receives interfaces.
    /// </summary>
    [DefaultExecutionOrder(-100)]
    public class GameInstaller : MonoBehaviour
    {
        public enum SaveBackend { File, Cloud }

        [SerializeField] private Player player;
        [SerializeField] private SaveBackend saveBackend = SaveBackend.File;
        [SerializeField] private bool muteAudioForTests;

        private void Awake()
        {
            if (player == null)
            {
                Debug.LogError("GameInstaller: Player reference is missing.", this);
                return;
            }

            ILogger logger = new UnityConsoleLogger();

            IAudioService audio = muteAudioForTests
                ? new SilentAudioService()
                : new UnityAudioService(GetComponent<AudioSource>());

            ISaveService save = saveBackend == SaveBackend.Cloud
                ? new CloudSaveService(logger)
                : new FileSaveService(logger);

            // Method injection: MonoBehaviours cannot use constructors.
            player.Inject(logger, audio, save);
        }
    }
}
4 files · namespace Patterns.Architectural.DependencyInjectionC# · UTF-8 · LF

Advantages & Disadvantages

+ Advantages

  • Classes depend on abstractions, so implementations can be swapped without touching consumers.
  • Dependencies are explicit in the constructor or Inject signature — no hidden Find calls.
  • Makes Edit Mode unit tests trivial: pass in fakes and assert on them.

− Disadvantages

  • MonoBehaviours cannot take constructor arguments, so you need method injection or a framework to bridge the gap.
  • The composition root can grow large, and wiring errors show up at runtime rather than compile time.
  • Over-abstracting everything behind interfaces adds indirection that makes simple code harder to read.

Tips

  1. 01Keep one composition root per scene (or one global boot installer). If many scripts are building their own services, the pattern has leaked.
  2. 02Use method injection (Inject(ILogger, IAudioService, ISaveService)) for MonoBehaviours and constructor injection for plain C# classes.
  3. 03Validate in the installer: throw early with a clear message if a required service is missing, instead of a NullReferenceException three scenes later.
  4. 04When the hand-written installer gets unwieldy, frameworks like VContainer or Zenject/Extenject automate binding, scoping and lifetime management.
  5. 05Avoid injecting the container itself into game classes — that quietly turns DI back into a Service Locator.