Modular system with DI

March 2025. Project grows. Everything in one folder, everything knows about everything. Scary to change.

Decided to make modules. Each module isolated. Dependencies explicit.

Structure

modules/
  auth/
    AuthModule.ts
    AuthService.ts
    AuthController.ts
  cms/
    CmsModule.ts
    CmsService.ts
    CmsController.ts

Module - a class that registers its services and controllers.

DI container

class AuthModule {
  register(container) {
    container.register('authService', AuthService)
    container.register('authController', AuthController)
  }
}

Service doesn't create dependencies itself. Gets them from container.

class AuthController {
  constructor(
    private authService: AuthService,
    private userService: UserService  // injected
  ) {}
}

What this gave

  • Testing: replace dependencies with mocks
  • Isolation: module doesn't touch others' internals
  • Order: clear where things are
  • Plugins: new functionality = new module

Challenges

Circular dependencies. AuthModule needs UserModule, UserModule needs AuthModule. Solution - extract common parts into separate module.

Took a week to refactor. But then adding new things became easier.

← Back to blog