DEV Community

Lộc Nguyễn
Lộc Nguyễn

Posted on

How a Wrong Singleton Implementation Crushed Our Redis Connections in NestJS

Singleton Pattern in NestJS: Don't Let Legacy Code Choke Your System

Hello everyone! Recently, my team noticed a significant performance drop after deploying new features. Despite scaling, the system became sluggish. After a deep dive, I discovered that the culprit was a "legacy" implementation of the Singleton pattern for our ClientProxy service (the service responsible for microservice communication via Redis).

Here is a breakdown of what went wrong and how we fixed it.


I. The Singleton Pattern

For those who might need a refresher: The Singleton pattern ensures that a class has only one instance throughout the application's lifecycle.

  • This instance is created once when the app starts.
  • All other services share this single instance instead of creating new ones, which is crucial for resource-heavy tasks like database or message broker connections.

II. Dependency Injection (DI) in NestJS

Singleton behavior is tightly coupled with Dependency Injection. In NestJS, Singleton is the default scope for DI.

  • During the bootstrap phase, NestJS registers these dependencies in the IoC (Inversion of Control) Container.
  • To use a service, you simply "inject" it via the class constructor, and NestJS handles the rest.

III. The "Legacy" Mistake: When DI Goes Wrong

In NestJS, the @Injectable() decorator defaults to a Singleton scope. If you need a service to be available everywhere (like Config, Logger, or a Shared Client), you can use the @Global() decorator.

The Issue We Faced:

Despite the service being marked as a Singleton, the legacy code was manually re-declaring the service in the providers array of multiple modules and overusing the @Inject() annotation unnecessarily.

The Result: Developers followed the "copy-paste" pattern. Every time a new service needed to talk to another microservice, it inadvertently triggered a new instance of ClientProxy. This led to an explosion of active connections to Redis, creating a massive bottleneck and slowing down the entire infrastructure.


IV. The Fix

The solution was straightforward but required a thorough cleanup:

  1. Centralized Module: Created a dedicated ClientProxyModule to house the ClientProxyService.
  2. Global Decorator: Added the @Global() decorator to this module so it’s initialized once at the root level.
  3. Refactoring: Removed redundant provider declarations and unnecessary @Inject() annotations from other services. We switched back to standard constructor injection.

V. Pro-Tips for Debugging

How do you know if you've handled Singletons correctly? Here are two quick tricks:

  • The Constructor Log: Add a console.log inside the constructor of your service. Restart the app. If you see that log more than once, you have an instantiation leak. It should only fire once during startup.
  • The Getter Log: If you use a getter method, add a log there to monitor exactly when and where the service is being accessed.

VI. Final Thoughts

Whether you are using NestJS, Spring Boot, or .NET, the core principle of the Singleton Pattern remains vital.

  • Understand the Scope: Don't just copy-paste. Understand how your framework manages instances.
  • Audit your Connections: Whether it's a Monolith or Microservices, "blind injection" can lead to resource exhaustion.

Check your current projects—are you injecting services recklessly? Let's keep our connections clean and our systems fast.

Happy coding! 🚀

Top comments (0)