name: service-locator-pattern description: "Implements a lightweight, type-safe Service Locator as a decoupling bridge. Includes a decision matrix for choosing between Service Locator, DI Container, and ScriptableObject Events." version: 2.0.0 tags: ["architecture", "service-locator", "dependency-management", "decoupling", "SOLID"] argument-hint: "action='implement' OR action='register' service='IAudioService'" tier: 2 requirements: unity_version: ">=6.0" render_pipeline: "Any" dependencies: [] context_discovery: check_unity_version: false check_render_pipeline: false scan_manifest_for: - "com.unity.vcontainer" - "com.neuecc.unirx" performance_budget: gc_alloc_per_frame: "0 bytes (dictionary lookup is O(1))" max_update_cost: "O(1) — resolve at Awake, cache reference" tdd_first: true disable-model-invocation: false user-invocable: true allowed-tools:
- run_command
- list_dir
- write_to_file
Service Locator Pattern
Overview
The Service Locator is a centralized registry of service interfaces. A consumer asks the Locator for IAudioService without knowing which concrete class implements it. This decouples systems without the setup complexity of a full DI Container. Use it as a stepping-stone when DI Containers are overkill, or when a project has no DI framework installed.
When to Use
Decision Matrix
| Scenario | Best Tool |
|---|---|
| Simple game, ≤3 cross-system dependencies | Service Locator ✅ |
| Solo/small team, no DI experience | Service Locator ✅ |
| Complex game, many cross-system dependencies | DI Container (VContainer) — use @di-container-manager |
| Gameplay events / cross-scene communication | ScriptableObject Events — use @event-bus-system |
| Only 1-2 consumers of a service | Direct reference injection (no locator needed) |
| Testing requires mock services | Service Locator ✅ (easy mock swap) OR DI Container |
Anti-Pattern Warning: Using a Service Locator heavily (>10 services) becomes a code smell — migrate to VContainer. The Locator is a bridge, not a foundation.
Architecture
┌─────────────────────────────────────────────────────────┐
│ SERVICE LOCATOR PATTERN │
├─────────────────────────────────────────────────────────┤
│ │
│ REGISTRATION (at boot) RESOLUTION │
│ ┌──────────────────────┐ ┌───────────────────┐ │
│ │ GameBootstrapper │ │ AudioController │ │
│ │ │ │ │ │
│ │ ServiceLocator │ │ _audio = Service │ │
│ │ .Register<IAudioSvc>│ │ Locator │ │
│ │ (new AudioService())│ │ .Get<IAudioSvc>()│ │
│ └──────────────────────┘ └───────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────────────────────────────────┐ │
│ │ ServiceLocator<T> │ │
│ │ Dictionary<Type, object> _services │ │
│ │ Register<T>(T service) where T : class │ │
│ │ Get<T>() where T : class │ │
│ │ Unregister<T>() │ │
│ └────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
TDD Contract
// Write these tests BEFORE implementing:
[Test]
public void Register_WhenServiceRegistered_GetReturnsIt()
{
var mockAudio = new MockAudioService();
ServiceLocator.Register<IAudioService>(mockAudio);
var resolved = ServiceLocator.Get<IAudioService>();
Assert.AreEqual(mockAudio, resolved);
ServiceLocator.Unregister<IAudioService>();
}
[Test]
public void Get_WhenServiceNotRegistered_ThrowsException()
{
Assert.Throws<KeyNotFoundException>(() => ServiceLocator.Get<IAudioService>());
}
Procedure
-
Define Interfaces: Create
IAudioService,ISaveService, etc. in aServices/Interfaces/folder. -
Implement Concrete Classes:
AudioService : MonoBehaviour, IAudioService. -
Create Bootstrapper: A single
GameBootstrapper.csregisters all services onAwake()in execution order (-100). -
Resolve at Awake: Consumers call
ServiceLocator.Get<IService>()in their ownAwake()orStart(). -
Cache the Reference: Store the resolved service in a private field. Never call
Get<T>()inUpdate().
Best Practices
- ✅ Register before consuming: Use
[DefaultExecutionOrder(-100)]on Bootstrapper. - ✅ Cache resolved services in
Awake():_audio = ServiceLocator.Get<IAudioService>(). - ✅ Use for cross-cutting concerns: Audio, Saving, Analytics, Localization.
- ✅ Swap mock services in Edit Mode tests for easy TDD.
- ❌ NEVER call
ServiceLocator.Get<T>()insideUpdate()— it's a dictionary lookup every frame. - ❌ NEVER register a service more than once without calling
Unregisterfirst. - ❌ NEVER use more than ~8 services — at that scale, migrate to
@di-container-manager.
Few-Shot Examples
Example 1: Core Implementation
User: "Implement a Service Locator for my game."
Agent:
// ServiceLocator.cs — place anywhere, no MonoBehaviour needed
using System;
using System.Collections.Generic;
public static class ServiceLocator
{
private static readonly Dictionary<Type, object> _services = new();
public static void Register<T>(T service) where T : class
{
var key = typeof(T);
if (_services.ContainsKey(key))
throw new InvalidOperationException($"Service {key.Name} is already registered. Call Unregister first.");
_services[key] = service;
}
public static T Get<T>() where T : class
{
var key = typeof(T);
if (!_services.TryGetValue(key, out var service))
throw new KeyNotFoundException($"Service {key.Name} not registered. Did you call Register<T>() in Bootstrapper?");
return (T)service;
}
public static bool TryGet<T>(out T service) where T : class
{
if (_services.TryGetValue(typeof(T), out var raw))
{
service = (T)raw;
return true;
}
service = null;
return false;
}
public static void Unregister<T>() where T : class => _services.Remove(typeof(T));
public static void Clear() => _services.Clear(); // Use in test teardown
}
Example 2: Bootstrapper
// GameBootstrapper.cs
using UnityEngine;
[DefaultExecutionOrder(-100)] // Runs before all other Awake calls
public class GameBootstrapper : MonoBehaviour
{
[SerializeField] private AudioService _audioService;
[SerializeField] private SaveSystem _saveSystem;
private void Awake()
{
ServiceLocator.Register<IAudioService>(_audioService);
ServiceLocator.Register<ISaveService>(_saveSystem);
DontDestroyOnLoad(gameObject);
}
private void OnDestroy()
{
ServiceLocator.Clear();
}
}
Example 3: Consumer
// PlayerController.cs
public class PlayerController : MonoBehaviour
{
private IAudioService _audio;
private void Awake()
{
// Resolve ONCE, cache forever
_audio = ServiceLocator.Get<IAudioService>();
}
private void OnJump()
{
_audio.PlayOneShot(AudioClipId.Jump);
}
}
Related Skills
-
@di-container-manager— Upgrade path when Service Locator outgrows its scope -
@event-bus-system— Alternative for broadcast communication (no direct reference needed) -
@advanced-game-bootstrapper— Bootstrapper lifecycle management -
@automated-unit-testing— Mock service injection for TDD
Template Files
-
templates/ServiceLocator.cs.txt— Static registry implementation -
templates/GameBootstrapper.cs.txt— Registration entry point -
templates/IServiceExample.cs.txt— Sample service interface
chat Comments (0)
Sign in to join the discussion and leave a comment.
Skill Details
Related Skills
Build your own?
Join 12,000+ developers contributing to the Claude ecosystem.
No comments yet. Be the first to share your thoughts!