Skip to main content

NestJS Dependency Injection Pitfalls: A Tale of Duplicate Providers

· 2 min read
Max Kaido
Architect

The Scenario

Today, I encountered a classic dependency injection issue while integrating a new tournament system with artifacts tracking in our Mercury Bot project. The error was subtle but instructive:

[Nest] 2991604  - 02/03/2025, 6:01:58 PM   ERROR [ExceptionHandler] Cannot read properties of undefined (reading 'get')
TypeError: Cannot read properties of undefined (reading 'get')

The Root Cause

The issue stemmed from a common anti-pattern in NestJS applications: duplicate provider registrations across multiple modules. Specifically:

  1. The same providers (ArtifactsService, Octokit) were registered in multiple modules
  2. Queue configurations were redundantly declared
  3. Module dependencies weren't properly structured

The Wrong Approach

Here's what the problematic code looked like:

@Module({
imports: [/* ... */],
providers: [
// Duplicate providers that were already available through ArtifactsModule
ArtifactsService,
Octokit,
// Other providers
]
})

The issue here is that we were trying to provide the same service in multiple places, leading to dependency injection conflicts and potentially undefined behavior.

The Solution

The solution involved following these key principles:

  1. Single Responsibility: Let each module manage its own providers

    // ArtifactsModule handles its own providers
    @Module({
    providers: [ArtifactsService, Octokit],
    exports: [ArtifactsService]
    })
  2. Proper Module Imports: Instead of re-declaring providers, import the modules that provide them

    @Module({
    imports: [
    ArtifactsModule,
    TournamentModule
    ]
    })
  3. Queue Registration: Let each module handle its own queue registration

    // In the module that owns the queue
    BullModule.registerQueue({
    name: QueueName.TOURNAMENT,
    });

Key Takeaways

  1. Provider Uniqueness: A provider should be declared in exactly one module and exported if needed elsewhere.

  2. Module Boundaries: Respect module boundaries - if you need functionality from another module, import that module rather than recreating its providers.

  3. Configuration Management: Global configurations (like ConfigModule) should be configured once and marked as global if needed everywhere.

  4. Queue Registration: Each module should register only the queues it directly uses, avoiding duplicate registrations.

Best Practices Moving Forward

  1. Before adding a provider to a module, check if it's already available through an imported module.

  2. Use the @Injectable() decorator's scope property when you need multiple instances.

  3. Keep module dependencies clean and explicit - avoid circular dependencies.

  4. Document module dependencies and exports clearly.

Real-World Impact

This issue manifested in our tournament system integration, where the ConfigService was undefined due to provider conflicts. The fix not only resolved the immediate error but also:

  • Reduced code duplication
  • Clarified module responsibilities
  • Improved application startup performance
  • Made the codebase more maintainable

Conclusion

Dependency injection is a powerful feature of NestJS, but it requires careful attention to provider uniqueness and module boundaries. When in doubt, follow the principle of single responsibility and let each module manage its own providers.

Remember: Just because TypeScript compiles doesn't mean your dependency injection is configured correctly. Always test your module integration thoroughly and pay attention to provider scope and registration.


This post is part of our ongoing series about real-world lessons learned while building Mercury Bot, a sophisticated market analysis system.