Skip to main content

On This Page

Architectural Shift: Replacing Singletons with Dependency Injection for Testable Code

2 min read
Share

These articles are AI-generated summaries. Please check the original sources for full details.

Why I Stopped Using Singletons (And How It Saved Our Architecture and My Sanity)

Software engineer Utkuhan Akar identified that global Singletons were causing random test failures due to state leakage. The team discovered that these ‘convenient’ patterns were actually hiding dependencies and doubling coffee consumption during multi-threading issues.

Why This Matters

The reliance on Singletons creates a technical debt cycle where the convenience of global access is offset by zero testability and hidden coupling. From a business perspective, this architectural choice increases maintenance costs and slows development velocity as changing one module inadvertently breaks another via shared global state.

Key Insights

  • Fact: Automated test suites failed randomly because Singletons leaked state between tests like a broken pipeline (Source: Utkuhan Akar).
  • Concept: Explicit Dependency Injection via constructor demands (e.g., IGameManager) creates self-documenting code and clear system boundaries.
  • Concept: The ‘Composition Root’ allows developers to swap implementations, such as local versus cloud save systems, by modifying a single line of code.
  • Tool: Industry-standard service providers are preferred over custom-built DI containers, which frequently suffer from memory leaks.

Working Examples

The ‘Convenient Trap’ where dependencies are hidden inside methods, making isolation impossible.

public class PlayerController {\n  public void TakeDamage(int amount) {\n    GameManager.Instance.ReduceScore(amount);\n  }\n}

Refactored code using explicit Dependency Injection and interfaces to define clear boundaries.

public interface IGameManager {\n  void ReduceScore(int amount);\n}\n\npublic class PlayerController {\n  private readonly IGameManager _gameManager;\n\n  public PlayerController(IGameManager gameManager) {\n    _gameManager = gameManager ?? throw new ArgumentNullException(nameof(gameManager));\n  }\n\n  public void TakeDamage(int amount) {\n    _gameManager.ReduceScore(amount);\n  }\n}

Practical Applications

  • Parallel CI/CD Pipelines: Removing shared state allows unit tests to run in parallel without side effects, ensuring reliable green checkmarks.
  • Onboarding Velocity: New developers can inspect class constructors to understand requirements immediately without tracing global instance calls.
  • Pitfall: Reinventing the wheel by building custom DI containers often leads to memory-leaking monsters in production environments.
  • Pitfall: Hiding dependencies inside methods forces the inclusion of the entire game state when testing a single component.

References:

Continue reading

Next article

Securing AI Agents: Governance and Guardrails for MCP-Enabled Coding Assistants

Related Content