Appearance
004 - Module Self-Containment Architecture
Status
Accepted
Date
2026-02-27
Context
The original architecture had AppModule owning TypeORM data sources for all feature modules, global guards registered as raw providers in AppModule, and cross-cutting services (HashingService, EncryptionService) not properly wired as a @Global() module. This created tight coupling: every new feature module required changes to AppModule, and the DI container failed at runtime because useExisting aliases were registered without their concrete classes.
Additionally, core/ contained guards and decorators but was not a NestJS module, so its providers had to be manually registered in AppModule.
Decision
SharedModule(@Global()): Owns cross-cutting infrastructure services (hashing, encryption). Registers concrete classes before symbol aliases souseExistingresolves correctly.CoreModule(@Global()): OwnsAPP_GUARDproviders (JwtAuthGuard,AuthTypeGuard). Decorators stay incore/decorators/as pure functions (no DI needed).Feature modules (
IdentityModule,NotificationsModule): Each owns itsTypeOrmModule.forRootAsync(...)data source andTypeOrmModule.forFeature(...). Modules do not exportTypeOrmModule.AppModule: Pure orchestrator — importsAppConfigModule,SharedModule,CoreModule,RouterModule, and feature modules. No providers.Refresh endpoint consolidation: Single
POST /identity/auth/refreshreplaces duplicate per-actor endpoints.Template registry port:
TemplateRegistryServicein notifications follows the port/adapter pattern viaITemplateRegistryServiceinterface, eliminating the DDD violation of a command handler importing directly from infrastructure.
Alternatives Considered
- Keep data sources in AppModule: Simpler initial setup but violates module encapsulation and forces AppModule to know every entity list.
- Dynamic modules for core guards: Overhead not justified for two global guards.
Consequences
- Each feature module is fully self-contained and can be tested in isolation.
AppModuleis minimal and stable across feature additions.- Runtime DI crash from broken
useExistingwiring is fixed. POST /identity/auth/user/refreshandPOST /identity/auth/client/refreshare removed in favor ofPOST /identity/auth/refresh.
Related Docs
- Feature docs:
apps/docs/features/api/auth/identity-auth-service-ports.md
Source Paths
apps/api/src/app.module.tsapps/api/src/core/core.module.tsapps/api/src/shared/shared.module.tsapps/api/src/modules/identity/identity.module.tsapps/api/src/modules/notifications/notifications.module.tsapps/api/src/modules/notifications/application/services/template-registry.service.interface.ts