9 Services und Dependency Injection

Services bilden das Rückgrat jeder NestJS-Anwendung und implementieren die Geschäftslogik, während das Dependency Injection (DI) System für lose gekoppelte, testbare und wartbare Architekturen sorgt. NestJS nutzt ein ausgeklügeltes DI-System, das auf Dekoratoren und Metadaten basiert und zur Compile-Zeit sowie zur Laufzeit Provider-Abhängigkeiten auflöst.

9.1 Einführung in Services

Services in NestJS sind spezielle Klassen, die mit dem @Injectable()-Dekorator annotiert sind und geschäftsspezifische Logik kapseln. Sie folgen dem Single Responsibility Principle und abstrahieren komplexe Operationen von Controllern. Services können andere Services als Abhängigkeiten haben und werden vom NestJS IoC-Container verwaltet.

9.1.1 Die Rolle von Services in der Architektur

Services erfüllen verschiedene zentrale Funktionen in NestJS-Anwendungen:

Geschäftslogik-Kapselung: Services implementieren die Kerngeschäftsregeln und -prozesse der Anwendung. Sie enthalten die domänenspezifische Logik, die unabhängig von der Präsentationsschicht funktioniert.

Datenzugriff-Abstraktion: Services abstrahieren den Zugriff auf Datenquellen wie Datenbanken, externe APIs oder Dateisysteme. Sie stellen eine konsistente Schnittstelle für Datenoperationen bereit.

Cross-Cutting Concerns: Services handhaben übergreifende Belange wie Logging, Caching, Validierung, E-Mail-Versand oder Authentifizierung, die von mehreren Teilen der Anwendung genutzt werden.

Integration-Layer: Services fungieren als Integrations-Layer für externe Systeme und APIs, wobei sie die Komplexität der Integration vor den aufrufenden Komponenten verbergen.

9.1.2 Service-Lebenszyklus

Services werden vom NestJS IoC-Container verwaltet und haben einen definierten Lebenszyklus:

import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';

@Injectable()
export class UserService implements OnModuleInit, OnModuleDestroy {
  private connectionPool: any;

  constructor(
    private readonly configService: ConfigService,
    private readonly logger: LoggerService,
  ) {}

  async onModuleInit() {
    this.logger.log('UserService is being initialized...');
    // Initialisierungslogik, z.B. Datenbankverbindungen aufbauen
    this.connectionPool = await this.createConnectionPool();
  }

  async onModuleDestroy() {
    this.logger.log('UserService is being destroyed...');
    // Aufräumarbeiten, z.B. Verbindungen schließen
    if (this.connectionPool) {
      await this.connectionPool.close();
    }
  }

  private async createConnectionPool() {
    const config = this.configService.get('database');
    // Connection Pool Logik
    return createPool(config);
  }
}

9.1.3 Service-Kategorien

Services lassen sich in verschiedene Kategorien einteilen:

Domain Services: Implementieren kerngeschäftsspezifische Logik und Regeln. Sie sind eng mit der Geschäftsdomäne verbunden und enthalten oft komplexe Algorithmen oder Workflows.

Application Services: Koordinieren mehrere Domain Services und implementieren Anwendungsfälle (Use Cases). Sie orchestrieren den Datenfluss zwischen verschiedenen Domänen-Komponenten.

Infrastructure Services: Stellen technische Funktionalitäten bereit wie Datenbank-Zugriff, E-Mail-Versand, File-Handling oder externe API-Calls.

Utility Services: Bieten wiederverwendbare Hilfsfunktionen wie Formatierung, Validierung, Kryptographie oder Datum-/Zeit-Operationen.

9.2 Grundlegende Konzepte des DI-Systems in NestJS

Das Dependency Injection System von NestJS ist inspiriert von Angular und nutzt TypeScript-Metadaten für automatische Abhängigkeitsauflösung. Es basiert auf mehreren Kernkonzepten, die zusammenarbeiten, um lose gekoppelte Architekturen zu ermöglichen.

9.2.1 Provider

Provider sind die grundlegenden Bausteine des DI-Systems. Sie definieren, wie Abhängigkeiten erstellt und bereitgestellt werden. Jeder Provider hat einen eindeutigen Token, der ihn identifiziert.

9.2.1.1 Provider-Arten

Class Provider - Die häufigste Form:

// Kurzform
@Module({
  providers: [UserService],
})
export class UserModule {}

// Vollständige Form
@Module({
  providers: [
    {
      provide: UserService,
      useClass: UserService,
    },
  ],
})
export class UserModule {}

// Alternative Implementation
@Module({
  providers: [
    {
      provide: UserService,
      useClass: EnhancedUserService, // Andere Implementierung
    },
  ],
})
export class UserModule {}

Value Provider - Für Konstanten und Konfigurationswerte:

@Module({
  providers: [
    {
      provide: 'API_VERSION',
      useValue: 'v1.0.0',
    },
    {
      provide: 'DATABASE_CONFIG',
      useValue: {
        host: 'localhost',
        port: 5432,
        database: 'myapp',
      },
    },
    {
      provide: 'FEATURE_FLAGS',
      useValue: {
        enableNewFeature: true,
        enableBetaFeatures: false,
      },
    },
  ],
})
export class ConfigModule {}

Factory Provider - Für dynamische Provider-Erstellung:

@Module({
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: async (configService: ConfigService) => {
        const config = configService.get('database');
        const connection = await createConnection(config);
        return connection;
      },
      inject: [ConfigService],
    },
    {
      provide: 'LOGGER',
      useFactory: (configService: ConfigService) => {
        const logLevel = configService.get('LOG_LEVEL') || 'info';
        return new Logger(logLevel);
      },
      inject: [ConfigService],
    },
  ],
})
export class DatabaseModule {}

Existing Provider - Für Aliasing:

@Module({
  providers: [
    UserService,
    {
      provide: 'IUserService',
      useExisting: UserService,
    },
  ],
})
export class UserModule {}

9.2.1.2 Async Provider

Für Provider, die asynchrone Initialisierung benötigen:

@Module({
  providers: [
    {
      provide: 'REDIS_CLIENT',
      useFactory: async (configService: ConfigService) => {
        const redis = new Redis({
          host: configService.get('REDIS_HOST'),
          port: configService.get('REDIS_PORT'),
          password: configService.get('REDIS_PASSWORD'),
        });
        
        await redis.ping(); // Verbindung testen
        return redis;
      },
      inject: [ConfigService],
    },
    {
      provide: 'EMAIL_TRANSPORT',
      useFactory: async (configService: ConfigService) => {
        const transporter = nodemailer.createTransporter({
          host: configService.get('SMTP_HOST'),
          port: configService.get('SMTP_PORT'),
          auth: {
            user: configService.get('SMTP_USER'),
            pass: configService.get('SMTP_PASS'),
          },
        });
        
        await transporter.verify(); // SMTP-Verbindung testen
        return transporter;
      },
      inject: [ConfigService],
    },
  ],
})
export class InfrastructureModule {}

9.2.2 Injektoren (Injectors)

Injektoren sind die Laufzeit-Systeme, die Provider instantiieren und Abhängigkeiten auflösen. NestJS erstellt eine Hierarchie von Injektoren basierend auf der Modul-Struktur.

9.2.2.1 Injector-Hierarchie

// Root-Level Injector
@Module({
  imports: [SharedModule],
  providers: [AppService],
})
export class AppModule {}

// Feature-Level Injector
@Module({
  providers: [
    UserService,
    {
      provide: 'USER_REPOSITORY',
      useClass: DatabaseUserRepository,
    },
  ],
})
export class UserModule {}

// Service mit Injection
@Injectable()
export class UserService {
  constructor(
    @Inject('USER_REPOSITORY') private userRepository: UserRepository,
    private configService: ConfigService, // Von SharedModule
  ) {}
}

9.2.2.2 Custom Injector-Scopes

// Verschiedene Injection-Scopes
@Injectable({ scope: Scope.DEFAULT }) // Singleton (Standard)
export class SingletonService {}

@Injectable({ scope: Scope.REQUEST }) // Pro Request
export class RequestScopedService {}

@Injectable({ scope: Scope.TRANSIENT }) // Neue Instanz bei jeder Injection
export class TransientService {}

9.2.3 Tokens und Custom Provider

Tokens identifizieren Provider eindeutig und ermöglichen flexible Dependency Injection-Patterns.

9.2.3.1 String Tokens

// Definition
@Module({
  providers: [
    {
      provide: 'PAYMENT_GATEWAY',
      useClass: StripePaymentGateway,
    },
    {
      provide: 'EMAIL_SERVICE',
      useClass: SendGridEmailService,
    },
  ],
  exports: ['PAYMENT_GATEWAY', 'EMAIL_SERVICE'],
})
export class IntegrationModule {}

// Injection
@Injectable()
export class OrderService {
  constructor(
    @Inject('PAYMENT_GATEWAY') private paymentGateway: PaymentGateway,
    @Inject('EMAIL_SERVICE') private emailService: EmailService,
  ) {}
}

9.2.3.2 Symbol Tokens

// Token-Definitionen
export const CACHE_MANAGER = Symbol('CACHE_MANAGER');
export const EVENT_BUS = Symbol('EVENT_BUS');
export const METRICS_COLLECTOR = Symbol('METRICS_COLLECTOR');

@Module({
  providers: [
    {
      provide: CACHE_MANAGER,
      useClass: RedisCache,
    },
    {
      provide: EVENT_BUS,
      useClass: EventEmitterBus,
    },
  ],
})
export class CoreModule {}

// Injection mit Symbol Tokens
@Injectable()
export class UserService {
  constructor(
    @Inject(CACHE_MANAGER) private cache: Cache,
    @Inject(EVENT_BUS) private eventBus: EventBus,
  ) {}
}

9.2.3.3 Interface Tokens

// Interface Definition
export interface INotificationService {
  sendNotification(message: string, recipient: string): Promise<void>;
}

// Token für Interface
export const INotificationService = Symbol('INotificationService');

// Implementierungen
@Injectable()
export class EmailNotificationService implements INotificationService {
  async sendNotification(message: string, recipient: string): Promise<void> {
    // E-Mail-Implementierung
  }
}

@Injectable()
export class SmsNotificationService implements INotificationService {
  async sendNotification(message: string, recipient: string): Promise<void> {
    // SMS-Implementierung
  }
}

// Provider-Konfiguration
@Module({
  providers: [
    {
      provide: INotificationService,
      useClass: EmailNotificationService, // Standardimplementierung
    },
  ],
})
export class NotificationModule {}

// Conditional Provider basierend auf Environment
@Module({
  providers: [
    {
      provide: INotificationService,
      useClass: process.env.NODE_ENV === 'production' 
        ? EmailNotificationService 
        : MockNotificationService,
    },
  ],
})
export class NotificationModule {}

9.2.4 Provider-Scopes

Provider-Scopes bestimmen die Lebensdauer und den Sharing-Modus von Provider-Instanzen.

9.2.4.1 DEFAULT Scope (Singleton)

@Injectable({ scope: Scope.DEFAULT })
export class ConfigService {
  private config: any;

  constructor() {
    this.config = this.loadConfig();
  }

  get(key: string): any {
    return this.config[key];
  }

  private loadConfig() {
    // Konfiguration nur einmal laden
    return { /* config data */ };
  }
}

9.2.4.2 REQUEST Scope

@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {
  private data: Map<string, any> = new Map();

  set(key: string, value: any): void {
    this.data.set(key, value);
  }

  get(key: string): any {
    return this.data.get(key);
  }

  // Wird für jeden Request neu erstellt
}

// Request-scoped Provider propagiert an abhängige Services
@Injectable({ scope: Scope.REQUEST })
export class UserService {
  constructor(
    private requestContext: RequestContextService,
    private userRepository: UserRepository, // Wird auch REQUEST-scoped
  ) {}
}

9.2.4.3 TRANSIENT Scope

@Injectable({ scope: Scope.TRANSIENT })
export class UniqueIdGenerator {
  private id: string;

  constructor() {
    this.id = crypto.randomUUID();
  }

  getId(): string {
    return this.id;
  }
}

// Jede Injection erstellt eine neue Instanz
@Injectable()
export class DocumentService {
  constructor(
    private idGenerator1: UniqueIdGenerator, // Instanz 1
    private idGenerator2: UniqueIdGenerator, // Instanz 2 (unterschiedlich)
  ) {}
}

9.3 Service-Erstellung und -Nutzung

9.3.1 Grundstruktur eines Services

Ein gut strukturierter Service folgt bewährten Design-Patterns und Prinzipien:

// src/users/services/user.service.ts
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../entities/user.entity';
import { CreateUserDto } from '../dto/create-user.dto';
import { UpdateUserDto } from '../dto/update-user.dto';
import { UserQueryDto } from '../dto/user-query.dto';
import { PaginationResult } from '../../common/interfaces/pagination.interface';
import { LoggerService } from '../../shared/services/logger.service';
import { CacheService } from '../../shared/services/cache.service';
import { EventEmitter2 } from '@nestjs/event-emitter';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
    private readonly logger: LoggerService,
    private readonly cacheService: CacheService,
    private readonly eventEmitter: EventEmitter2,
  ) {}

  async create(createUserDto: CreateUserDto): Promise<User> {
    this.logger.log(`Creating user with email: ${createUserDto.email}`);

    // Geschäftslogik-Validierung
    await this.validateUniqueEmail(createUserDto.email);

    try {
      const user = this.userRepository.create(createUserDto);
      const savedUser = await this.userRepository.save(user);

      // Event emittieren
      this.eventEmitter.emit('user.created', {
        userId: savedUser.id,
        email: savedUser.email,
      });

      // Cache invalidieren
      await this.cacheService.del('users:all');

      this.logger.log(`User created successfully: ${savedUser.id}`);
      return savedUser;
    } catch (error) {
      this.logger.error(`Failed to create user: ${error.message}`, error.stack);
      throw error;
    }
  }

  async findAll(query: UserQueryDto): Promise<PaginationResult<User>> {
    const cacheKey = `users:${JSON.stringify(query)}`;
    const cached = await this.cacheService.get(cacheKey);
    
    if (cached) {
      return cached;
    }

    const queryBuilder = this.userRepository.createQueryBuilder('user');

    // Filtering
    if (query.search) {
      queryBuilder.andWhere(
        '(user.firstName ILIKE :search OR user.lastName ILIKE :search OR user.email ILIKE :search)',
        { search: `%${query.search}%` }
      );
    }

    if (query.role) {
      queryBuilder.andWhere('user.role = :role', { role: query.role });
    }

    if (query.isActive !== undefined) {
      queryBuilder.andWhere('user.isActive = :isActive', { isActive: query.isActive });
    }

    // Pagination
    const skip = (query.page - 1) * query.limit;
    queryBuilder.skip(skip).take(query.limit);

    // Sorting
    queryBuilder.orderBy(`user.${query.sortBy}`, query.sortOrder);

    const [users, total] = await queryBuilder.getManyAndCount();

    const result = {
      data: users,
      total,
      page: query.page,
      limit: query.limit,
      totalPages: Math.ceil(total / query.limit),
    };

    // Cache für 5 Minuten
    await this.cacheService.set(cacheKey, result, 300);

    return result;
  }

  async findOne(id: string): Promise<User> {
    const cacheKey = `user:${id}`;
    const cached = await this.cacheService.get(cacheKey);
    
    if (cached) {
      return cached;
    }

    const user = await this.userRepository.findOne({
      where: { id },
      relations: ['profile', 'roles'],
    });

    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    await this.cacheService.set(cacheKey, user, 600); // 10 Minuten
    return user;
  }

  async update(id: string, updateUserDto: UpdateUserDto): Promise<User> {
    const user = await this.findOne(id);

    // E-Mail-Eindeutigkeit prüfen falls E-Mail geändert wird
    if (updateUserDto.email && updateUserDto.email !== user.email) {
      await this.validateUniqueEmail(updateUserDto.email);
    }

    Object.assign(user, updateUserDto);
    const updatedUser = await this.userRepository.save(user);

    // Events und Cache
    this.eventEmitter.emit('user.updated', { userId: id, changes: updateUserDto });
    await this.cacheService.del(`user:${id}`);
    await this.cacheService.del('users:all');

    return updatedUser;
  }

  async remove(id: string): Promise<void> {
    const user = await this.findOne(id);
    
    await this.userRepository.remove(user);
    
    this.eventEmitter.emit('user.deleted', { userId: id });
    await this.cacheService.del(`user:${id}`);
    await this.cacheService.del('users:all');
  }

  // Private Hilfsmethoden
  private async validateUniqueEmail(email: string): Promise<void> {
    const existingUser = await this.userRepository.findOne({ where: { email } });
    if (existingUser) {
      throw new ConflictException(`User with email ${email} already exists`);
    }
  }

  // Geschäftsspezifische Methoden
  async activateUser(id: string): Promise<User> {
    const user = await this.findOne(id);
    user.isActive = true;
    user.activatedAt = new Date();
    
    const updatedUser = await this.userRepository.save(user);
    this.eventEmitter.emit('user.activated', { userId: id });
    
    return updatedUser;
  }

  async deactivateUser(id: string): Promise<User> {
    const user = await this.findOne(id);
    user.isActive = false;
    user.deactivatedAt = new Date();
    
    const updatedUser = await this.userRepository.save(user);
    this.eventEmitter.emit('user.deactivated', { userId: id });
    
    return updatedUser;
  }
}

9.3.2 Der @Injectable-Dekorator

Der @Injectable()-Dekorator markiert eine Klasse als Provider und ermöglicht Dependency Injection:

// Einfacher Injectable Service
@Injectable()
export class MathService {
  add(a: number, b: number): number {
    return a + b;
  }

  multiply(a: number, b: number): number {
    return a * b;
  }
}

// Injectable mit Scope
@Injectable({ scope: Scope.REQUEST })
export class RequestService {
  private startTime = Date.now();

  getRequestDuration(): number {
    return Date.now() - this.startTime;
  }
}

// Injectable mit Interface Implementation
export interface ICacheService {
  get(key: string): Promise<any>;
  set(key: string, value: any, ttl?: number): Promise<void>;
  del(key: string): Promise<void>;
}

@Injectable()
export class RedisCacheService implements ICacheService {
  constructor(@Inject('REDIS_CLIENT') private redis: Redis) {}

  async get(key: string): Promise<any> {
    const value = await this.redis.get(key);
    return value ? JSON.parse(value) : null;
  }

  async set(key: string, value: any, ttl = 3600): Promise<void> {
    await this.redis.setex(key, ttl, JSON.stringify(value));
  }

  async del(key: string): Promise<void> {
    await this.redis.del(key);
  }
}

9.3.3 Service-Hierarchie und Provider-Ebenen

Services können in verschiedenen Modulebenen registriert und in einer Hierarchie organisiert werden:

// Root-Level Services (Global)
@Module({
  providers: [
    {
      provide: 'GLOBAL_CONFIG',
      useValue: globalConfig,
    },
    LoggerService,
  ],
  exports: ['GLOBAL_CONFIG', LoggerService],
  global: true,
})
export class CoreModule {}

// Feature-Level Services
@Module({
  providers: [
    UserService,
    UserValidationService,
    {
      provide: 'USER_CACHE_TTL',
      useValue: 3600,
    },
  ],
  exports: [UserService],
})
export class UserModule {}

// Sub-Feature Services
@Module({
  providers: [
    UserProfileService,
    UserPreferencesService,
  ],
  exports: [UserProfileService, UserPreferencesService],
})
export class UserProfileModule {}

// Service mit hierarchischen Dependencies
@Injectable()
export class UserProfileService {
  constructor(
    private userService: UserService, // Von UserModule
    private logger: LoggerService, // Von CoreModule (global)
    @Inject('USER_CACHE_TTL') private cacheTtl: number, // Von UserModule
  ) {}
}

9.4 Fortgeschrittene DI-Konzepte

9.4.1 Factory Provider

Factory Provider ermöglichen komplexe Provider-Erstellung mit dynamischer Konfiguration:

// Einfache Factory
@Module({
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: (configService: ConfigService) => {
        const dbConfig = configService.get('database');
        return createConnection(dbConfig);
      },
      inject: [ConfigService],
    },
  ],
})
export class DatabaseModule {}

// Async Factory
@Module({
  providers: [
    {
      provide: 'EMAIL_TRANSPORT',
      useFactory: async (configService: ConfigService, logger: LoggerService) => {
        const emailConfig = configService.get('email');
        
        logger.log('Initializing email transport...');
        
        const transporter = nodemailer.createTransporter(emailConfig);
        await transporter.verify();
        
        logger.log('Email transport initialized successfully');
        return transporter;
      },
      inject: [ConfigService, LoggerService],
    },
  ],
})
export class EmailModule {}

// Conditional Factory
@Module({
  providers: [
    {
      provide: 'STORAGE_SERVICE',
      useFactory: (configService: ConfigService) => {
        const storageType = configService.get('STORAGE_TYPE');
        
        switch (storageType) {
          case 'aws':
            return new AWSStorageService(configService.get('aws'));
          case 'gcp':
            return new GCPStorageService(configService.get('gcp'));
          case 'local':
            return new LocalStorageService(configService.get('local'));
          default:
            throw new Error(`Unsupported storage type: ${storageType}`);
        }
      },
      inject: [ConfigService],
    },
  ],
})
export class StorageModule {}

// Factory mit Multiple Dependencies
@Module({
  providers: [
    {
      provide: 'PAYMENT_PROCESSOR',
      useFactory: async (
        configService: ConfigService,
        logger: LoggerService,
        metricsService: MetricsService,
      ) => {
        const paymentConfig = configService.get('payment');
        
        const processor = new PaymentProcessor({
          apiKey: paymentConfig.apiKey,
          environment: paymentConfig.environment,
          logger,
          metrics: metricsService,
        });
        
        await processor.initialize();
        return processor;
      },
      inject: [ConfigService, LoggerService, MetricsService],
    },
  ],
})
export class PaymentModule {}

9.4.2 Async Provider

Async Provider für asynchrone Initialisierung:

// Async Provider mit Datenbankverbindung
@Module({
  providers: [
    {
      provide: 'MONGO_CONNECTION',
      useFactory: async (configService: ConfigService): Promise<MongoClient> => {
        const mongoUrl = configService.get('MONGO_URL');
        const client = new MongoClient(mongoUrl);
        
        await client.connect();
        console.log('MongoDB connected successfully');
        
        return client;
      },
      inject: [ConfigService],
    },
  ],
})
export class MongoModule {}

// Async Provider mit External API
@Module({
  providers: [
    {
      provide: 'EXTERNAL_API_CLIENT',
      useFactory: async (
        configService: ConfigService,
        httpService: HttpService,
      ): Promise<ExternalApiClient> => {
        const apiConfig = configService.get('externalApi');
        
        // API-Verfügbarkeit prüfen
        const healthCheck = await httpService
          .get(`${apiConfig.baseUrl}/health`)
          .toPromise();
        
        if (healthCheck.status !== 200) {
          throw new Error('External API is not available');
        }
        
        return new ExternalApiClient(apiConfig);
      },
      inject: [ConfigService, HttpService],
    },
  ],
})
export class ExternalApiModule {}

// Async Provider mit Retry-Logik
@Module({
  providers: [
    {
      provide: 'REDIS_CONNECTION',
      useFactory: async (configService: ConfigService) => {
        const redisConfig = configService.get('redis');
        let attempts = 0;
        const maxAttempts = 3;
        
        while (attempts < maxAttempts) {
          try {
            const client = new Redis(redisConfig);
            await client.ping();
            console.log('Redis connected successfully');
            return client;
          } catch (error) {
            attempts++;
            console.log(`Redis connection attempt ${attempts} failed: ${error.message}`);
            
            if (attempts === maxAttempts) {
              throw new Error('Failed to connect to Redis after maximum attempts');
            }
            
            await new Promise(resolve => setTimeout(resolve, 1000 * attempts));
          }
        }
      },
      inject: [ConfigService],
    },
  ],
})
export class RedisModule {}

9.4.3 Optional Dependencies

Für optionale Abhängigkeiten, die möglicherweise nicht verfügbar sind:

import { Optional } from '@nestjs/common';

@Injectable()
export class NotificationService {
  constructor(
    @Optional() private emailService?: EmailService,
    @Optional() private smsService?: SmsService,
    @Optional() @Inject('PUSH_NOTIFICATION_SERVICE') 
    private pushService?: PushNotificationService,
  ) {}

  async sendNotification(
    message: string, 
    recipient: string, 
    channels: string[] = ['email']
  ): Promise<void> {
    const results: Promise<void>[] = [];

    if (channels.includes('email') && this.emailService) {
      results.push(this.emailService.send(message, recipient));
    }

    if (channels.includes('sms') && this.smsService) {
      results.push(this.smsService.send(message, recipient));
    }

    if (channels.includes('push') && this.pushService) {
      results.push(this.pushService.send(message, recipient));
    }

    if (results.length === 0) {
      console.warn('No notification channels available');
      return;
    }

    await Promise.allSettled(results);
  }
}

// Conditional Provider Registration
@Module({
  providers: [
    NotificationService,
    // EmailService nur wenn konfiguriert
    ...(process.env.EMAIL_ENABLED === 'true' ? [EmailService] : []),
    // SMS Service nur in Production
    ...(process.env.NODE_ENV === 'production' ? [SmsService] : []),
  ],
})
export class NotificationModule {}

9.4.4 Circular Dependencies

Umgang mit zirkulären Abhängigkeiten:

// Problematisch: A → B → A
@Injectable()
export class ServiceA {
  constructor(private serviceB: ServiceB) {} // ServiceB benötigt ServiceA
}

@Injectable()
export class ServiceB {
  constructor(private serviceA: ServiceA) {} // Zirkuläre Abhängigkeit!
}

// Lösung 1: forwardRef verwenden
import { forwardRef } from '@nestjs/common';

@Injectable()
export class ServiceA {
  constructor(
    @Inject(forwardRef(() => ServiceB))
    private serviceB: ServiceB,
  ) {}
}

@Injectable()
export class ServiceB {
  constructor(
    @Inject(forwardRef(() => ServiceA))
    private serviceA: ServiceA,
  ) {}
}

// Lösung 2: Shared Service extrahieren
@Injectable()
export class SharedService {
  // Gemeinsame Funktionalität
}

@Injectable()
export class ServiceA {
  constructor(private sharedService: SharedService) {}
}

@Injectable()
export class ServiceB {
  constructor(private sharedService: SharedService) {}
}

// Lösung 3: Event-based Kommunikation
@Injectable()
export class ServiceA {
  constructor(private eventEmitter: EventEmitter2) {}

  async doSomething() {
    const result = await this.someOperation();
    this.eventEmitter.emit('service-a.completed', result);
    return result;
  }
}

@Injectable()
export class ServiceB {
  constructor(private eventEmitter: EventEmitter2) {}

  @OnEvent('service-a.completed')
  handleServiceACompletion(result: any) {
    // React to ServiceA completion
  }
}

// Lösung 4: Setter Injection (als letzter Ausweg)
@Injectable()
export class ServiceA {
  private serviceB: ServiceB;

  @Inject(forwardRef(() => ServiceB))
  setServiceB(serviceB: ServiceB) {
    this.serviceB = serviceB;
  }
}

9.5 Anwendungsbeispiele

9.5.1 Business Logic Services

Business Logic Services implementieren domänenspezifische Geschäftsregeln und Workflows:

// E-Commerce Order Service
@Injectable()
export class OrderService {
  constructor(
    @InjectRepository(Order) private orderRepository: Repository<Order>,
    private userService: UserService,
    private productService: ProductService,
    private paymentService: PaymentService,
    private inventoryService: InventoryService,
    private emailService: EmailService,
    private logger: LoggerService,
    private eventEmitter: EventEmitter2,
  ) {}

  async createOrder(createOrderDto: CreateOrderDto, userId: string): Promise<Order> {
    this.logger.log(`Creating order for user ${userId}`);

    // 1. Benutzer validieren
    const user = await this.userService.findOne(userId);
    if (!user.isActive) {
      throw new BadRequestException('User account is not active');
    }

    // 2. Produkte validieren und Preise abrufen
    const orderItems = await this.validateAndPriceItems(createOrderDto.items);
    
    // 3. Inventar prüfen
    await this.inventoryService.checkAvailability(orderItems);

    // 4. Gesamtpreis berechnen
    const totalAmount = this.calculateTotal(orderItems);
    
    // 5. Geschäftsregeln anwenden
    const discount = await this.calculateDiscount(user, totalAmount);
    const finalAmount = totalAmount - discount;

    // 6. Bestellung erstellen
    const order = this.orderRepository.create({
      user,
      items: orderItems,
      totalAmount,
      discountAmount: discount,
      finalAmount,
      status: OrderStatus.PENDING,
    });

    const savedOrder = await this.orderRepository.save(order);

    // 7. Inventar reservieren
    await this.inventoryService.reserveItems(orderItems, savedOrder.id);

    // 8. Events emittieren
    this.eventEmitter.emit('order.created', {
      orderId: savedOrder.id,
      userId,
      amount: finalAmount,
    });

    this.logger.log(`Order ${savedOrder.id} created successfully`);
    return savedOrder;
  }

  async processPayment(orderId: string, paymentDetails: PaymentDto): Promise<Order> {
    const order = await this.findOrderById(orderId);
    
    if (order.status !== OrderStatus.PENDING) {
      throw new BadRequestException('Order is not in pending status');
    }

    try {
      // Payment verarbeiten
      const paymentResult = await this.paymentService.processPayment({
        amount: order.finalAmount,
        currency: 'EUR',
        ...paymentDetails,
      });

      // Order-Status aktualisieren
      order.status = OrderStatus.PAID;
      order.paymentId = paymentResult.id;
      order.paidAt = new Date();

      const updatedOrder = await this.orderRepository.save(order);

      // Inventar bestätigen
      await this.inventoryService.confirmReservation(order.items, orderId);

      // Fulfillment starten
      this.eventEmitter.emit('order.paid', {
        orderId: updatedOrder.id,
        paymentId: paymentResult.id,
      });

      // Bestätigungs-E-Mail senden
      await this.emailService.sendOrderConfirmation(order);

      return updatedOrder;
    } catch (error) {
      // Payment fehlgeschlagen
      order.status = OrderStatus.PAYMENT_FAILED;
      order.paymentError = error.message;
      await this.orderRepository.save(order);

      // Inventar freigeben
      await this.inventoryService.releaseReservation(order.items, orderId);

      throw new BadRequestException(`Payment failed: ${error.message}`);
    }
  }

  private async validateAndPriceItems(items: CreateOrderItemDto[]): Promise<OrderItem[]> {
    const orderItems: OrderItem[] = [];

    for (const item of items) {
      const product = await this.productService.findOne(item.productId);
      
      if (!product.isActive) {
        throw new BadRequestException(`Product ${product.name} is not available`);
      }

      orderItems.push({
        product,
        quantity: item.quantity,
        unitPrice: product.price,
        totalPrice: product.price * item.quantity,
      });
    }

    return orderItems;
  }

  private calculateTotal(items: OrderItem[]): number {
    return items.reduce((total, item) => total + item.totalPrice, 0);
  }

  private async calculateDiscount(user: User, totalAmount: number): Promise<number> {
    let discount = 0;

    // Erste Bestellung Rabatt
    const isFirstOrder = await this.isFirstOrder(user.id);
    if (isFirstOrder) {
      discount += totalAmount * 0.1; // 10% Rabatt
    }

    // Volumen-Rabatt
    if (totalAmount > 500) {
      discount += totalAmount * 0.05; // 5% ab 500€
    }

    // VIP-Rabatt
    if (user.isVip) {
      discount += totalAmount * 0.15; // 15% für VIP
    }

    return Math.min(discount, totalAmount * 0.3); // Max 30% Rabatt
  }

  private async isFirstOrder(userId: string): Promise<boolean> {
    const orderCount = await this.orderRepository.count({
      where: { user: { id: userId } },
    });
    return orderCount === 0;
  }
}

9.5.2 Data Access Layer

Data Access Services abstrahieren Datenbankoperationen und implementieren Repository-Patterns:

// Generic Repository Base Service
@Injectable()
export abstract class BaseRepositoryService<T, CreateDto, UpdateDto> {
  constructor(
    protected readonly repository: Repository<T>,
    protected readonly logger: LoggerService,
  ) {}

  async create(createDto: CreateDto): Promise<T> {
    try {
      const entity = this.repository.create(createDto as any);
      return await this.repository.save(entity);
    } catch (error) {
      this.logger.error(`Failed to create entity: ${error.message}`, error.stack);
      throw error;
    }
  }

  async findAll(options?: any): Promise<T[]> {
    return this.repository.find(options);
  }

  async findOne(id: string): Promise<T> {
    const entity = await this.repository.findOne({ where: { id } } as any);
    if (!entity) {
      throw new NotFoundException(`Entity with ID ${id} not found`);
    }
    return entity;
  }

  async update(id: string, updateDto: UpdateDto): Promise<T> {
    const entity = await this.findOne(id);
    Object.assign(entity, updateDto);
    return this.repository.save(entity);
  }

  async remove(id: string): Promise<void> {
    const entity = await this.findOne(id);
    await this.repository.remove(entity);
  }

  async count(options?: any): Promise<number> {
    return this.repository.count(options);
  }

  async exists(id: string): Promise<boolean> {
    const count = await this.repository.count({ where: { id } } as any);
    return count > 0;
  }
}

// Specialized User Repository Service
@Injectable()
export class UserRepositoryService extends BaseRepositoryService<User, CreateUserDto, UpdateUserDto> {
  constructor(
    @InjectRepository(User) userRepository: Repository<User>,
    logger: LoggerService,
    private cacheService: CacheService,
  ) {
    super(userRepository, logger);
  }

  async findByEmail(email: string): Promise<User | null> {
    const cacheKey = `user:email:${email}`;
    const cached = await this.cacheService.get(cacheKey);
    
    if (cached) {
      return cached;
    }

    const user = await this.repository.findOne({
      where: { email },
      relations: ['profile', 'roles'],
    });

    if (user) {
      await this.cacheService.set(cacheKey, user, 600);
    }

    return user;
  }

  async findActiveUsers(pagination: PaginationDto): Promise<PaginationResult<User>> {
    const queryBuilder = this.repository
      .createQueryBuilder('user')
      .where('user.isActive = :isActive', { isActive: true })
      .leftJoinAndSelect('user.profile', 'profile');

    const skip = (pagination.page - 1) * pagination.limit;
    queryBuilder.skip(skip).take(pagination.limit);

    const [users, total] = await queryBuilder.getManyAndCount();

    return {
      data: users,
      total,
      page: pagination.page,
      limit: pagination.limit,
      totalPages: Math.ceil(total / pagination.limit),
    };
  }

  async getUserStats(): Promise<UserStats> {
    const [totalUsers, activeUsers, newUsersThisMonth] = await Promise.all([
      this.repository.count(),
      this.repository.count({ where: { isActive: true } }),
      this.repository.count({
        where: {
          createdAt: MoreThan(new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)),
        },
      }),
    ]);

    return {
      totalUsers,
      activeUsers,
      inactiveUsers: totalUsers - activeUsers,
      newUsersThisMonth,
    };
  }

  async bulkCreate(users: CreateUserDto[]): Promise<User[]> {
    const entities = users.map(user => this.repository.create(user));
    return this.repository.save(entities);
  }

  async softDelete(id: string): Promise<void> {
    await this.repository.softDelete(id);
    await this.cacheService.del(`user:${id}`);
  }
}

9.5.3 Utility Services

Utility Services stellen wiederverwendbare Hilfsfunktionen bereit:

// Cryptography Service
@Injectable()
export class CryptoService {
  private readonly algorithm = 'aes-256-gcm';
  private readonly keyLength = 32;
  private readonly ivLength = 16;
  private readonly tagLength = 16;

  constructor(
    @Inject('CRYPTO_SECRET') private secret: string,
    private logger: LoggerService,
  ) {}

  async hash(data: string, saltRounds = 12): Promise<string> {
    return bcrypt.hash(data, saltRounds);
  }

  async verifyHash(data: string, hash: string): Promise<boolean> {
    return bcrypt.compare(data, hash);
  }

  encrypt(text: string, key?: string): string {
    try {
      const keyBuffer = key ? 
        crypto.scryptSync(key, 'salt', this.keyLength) :
        crypto.scryptSync(this.secret, 'salt', this.keyLength);
      
      const iv = crypto.randomBytes(this.ivLength);
      const cipher = crypto.createCipher(this.algorithm, keyBuffer, { iv });
      
      let encrypted = cipher.update(text, 'utf8', 'hex');
      encrypted += cipher.final('hex');
      
      const tag = cipher.getAuthTag();
      
      return iv.toString('hex') + ':' + tag.toString('hex') + ':' + encrypted;
    } catch (error) {
      this.logger.error(`Encryption failed: ${error.message}`);
      throw new Error('Encryption failed');
    }
  }

  decrypt(encryptedData: string, key?: string): string {
    try {
      const parts = encryptedData.split(':');
      if (parts.length !== 3) {
        throw new Error('Invalid encrypted data format');
      }

      const [ivHex, tagHex, encryptedHex] = parts;
      const keyBuffer = key ? 
        crypto.scryptSync(key, 'salt', this.keyLength) :
        crypto.scryptSync(this.secret, 'salt', this.keyLength);
      
      const iv = Buffer.from(ivHex, 'hex');
      const tag = Buffer.from(tagHex, 'hex');
      const decipher = crypto.createDecipher(this.algorithm, keyBuffer, { iv });
      
      decipher.setAuthTag(tag);
      
      let decrypted = decipher.update(encryptedHex, 'hex', 'utf8');
      decrypted += decipher.final('utf8');
      
      return decrypted;
    } catch (error) {
      this.logger.error(`Decryption failed: ${error.message}`);
      throw new Error('Decryption failed');
    }
  }

  generateRandomString(length = 32): string {
    return crypto.randomBytes(length).toString('hex');
  }

  generateApiKey(): string {
    const timestamp = Date.now().toString(36);
    const random = this.generateRandomString(16);
    return `ak_${timestamp}_${random}`;
  }
}

// Date/Time Utility Service
@Injectable()
export class DateTimeService {
  private readonly defaultTimeZone = 'Europe/Berlin';

  formatDate(date: Date, format = 'YYYY-MM-DD', timezone?: string): string {
    return dayjs(date).tz(timezone || this.defaultTimeZone).format(format);
  }

  parseDate(dateString: string, format?: string): Date {
    const parsed = format ? dayjs(dateString, format) : dayjs(dateString);
    if (!parsed.isValid()) {
      throw new BadRequestException(`Invalid date format: ${dateString}`);
    }
    return parsed.toDate();
  }

  addDays(date: Date, days: number): Date {
    return dayjs(date).add(days, 'day').toDate();
  }

  subtractDays(date: Date, days: number): Date {
    return dayjs(date).subtract(days, 'day').toDate();
  }

  diffInDays(date1: Date, date2: Date): number {
    return dayjs(date1).diff(dayjs(date2), 'day');
  }

  isWeekend(date: Date): boolean {
    const dayOfWeek = dayjs(date).day();
    return dayOfWeek === 0 || dayOfWeek === 6; // Sunday or Saturday
  }

  getBusinessDays(startDate: Date, endDate: Date): Date[] {
    const businessDays: Date[] = [];
    let current = dayjs(startDate);
    const end = dayjs(endDate);

    while (current.isBefore(end) || current.isSame(end)) {
      if (!this.isWeekend(current.toDate())) {
        businessDays.push(current.toDate());
      }
      current = current.add(1, 'day');
    }

    return businessDays;
  }

  getStartOfDay(date: Date): Date {
    return dayjs(date).startOf('day').toDate();
  }

  getEndOfDay(date: Date): Date {
    return dayjs(date).endOf('day').toDate();
  }

  getCurrentTimestamp(): number {
    return Date.now();
  }

  isExpired(expiryDate: Date): boolean {
    return dayjs().isAfter(dayjs(expiryDate));
  }
}

// Validation Utility Service
@Injectable()
export class ValidationService {
  validateEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }

  validatePhoneNumber(phone: string, countryCode = 'DE'): boolean {
    try {
      const phoneNumber = parsePhoneNumber(phone, countryCode);
      return phoneNumber.isValid();
    } catch {
      return false;
    }
  }

  validatePassword(password: string): { isValid: boolean; errors: string[] } {
    const errors: string[] = [];

    if (password.length < 8) {
      errors.push('Password must be at least 8 characters long');
    }

    if (!/[A-Z]/.test(password)) {
      errors.push('Password must contain at least one uppercase letter');
    }

    if (!/[a-z]/.test(password)) {
      errors.push('Password must contain at least one lowercase letter');
    }

    if (!/\d/.test(password)) {
      errors.push('Password must contain at least one number');
    }

    if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
      errors.push('Password must contain at least one special character');
    }

    return {
      isValid: errors.length === 0,
      errors,
    };
  }

  validateUrl(url: string): boolean {
    try {
      new URL(url);
      return true;
    } catch {
      return false;
    }
  }

  validateUuid(uuid: string): boolean {
    const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
    return uuidRegex.test(uuid);
  }

  sanitizeInput(input: string): string {
    return input
      .trim()
      .replace(/[<>]/g, '') // Remove < and >
      .replace(/javascript:/gi, '') // Remove javascript: protocol
      .replace(/on\w+=/gi, ''); // Remove event handlers
  }

  validateIpAddress(ip: string): boolean {
    const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
    const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
    
    return ipv4Regex.test(ip) || ipv6Regex.test(ip);
  }
}

9.6 Best Practices für Services in NestJS

9.6.1 Service-Design-Prinzipien

Single Responsibility Principle: Jeder Service sollte eine klar definierte Verantwortlichkeit haben:

// Gut: Spezifischer Service für E-Mail-Funktionalität
@Injectable()
export class EmailService {
  async sendWelcomeEmail(user: User): Promise<void> {}
  async sendPasswordResetEmail(user: User, token: string): Promise<void> {}
  async sendOrderConfirmation(order: Order): Promise<void> {}
}

// Schlecht: Zu viele Verantwortlichkeiten
@Injectable()
export class NotificationAndEmailAndSmsAndPushService {
  // Zu viele verschiedene Aufgaben
}

// Besser: Separate Services mit klaren Verantwortlichkeiten
@Injectable()
export class EmailService { /* E-Mail-Logik */ }

@Injectable()
export class SmsService { /* SMS-Logik */ }

@Injectable()
export class PushNotificationService { /* Push-Notification-Logik */ }

@Injectable()
export class NotificationOrchestratorService {
  constructor(
    private emailService: EmailService,
    private smsService: SmsService,
    private pushService: PushNotificationService,
  ) {}
  
  async sendMultiChannelNotification(message: NotificationMessage): Promise<void> {
    // Orchestriert verschiedene Notification-Services
  }
}

Interface Segregation: Verwenden Sie spezifische Interfaces:

// Spezifische Interfaces für verschiedene Aspekte
export interface IUserReader {
  findById(id: string): Promise<User>;
  findByEmail(email: string): Promise<User>;
}

export interface IUserWriter {
  create(user: CreateUserDto): Promise<User>;
  update(id: string, user: UpdateUserDto): Promise<User>;
  delete(id: string): Promise<void>;
}

export interface IUserValidator {
  validateEmail(email: string): boolean;
  validatePassword(password: string): ValidationResult;
}

// Service implementiert nur benötigte Interfaces
@Injectable()
export class UserService implements IUserReader, IUserWriter {
  // Implementierung
}

@Injectable()
export class UserValidationService implements IUserValidator {
  // Implementierung
}

9.6.2 Error Handling in Services

// Custom Exceptions für Service-Layer
export class UserNotFoundError extends Error {
  constructor(identifier: string) {
    super(`User not found: ${identifier}`);
    this.name = 'UserNotFoundError';
  }
}

export class DuplicateEmailError extends Error {
  constructor(email: string) {
    super(`Email already exists: ${email}`);
    this.name = 'DuplicateEmailError';
  }
}

@Injectable()
export class UserService {
  async findById(id: string): Promise<User> {
    try {
      const user = await this.userRepository.findOne({ where: { id } });
      if (!user) {
        throw new UserNotFoundError(id);
      }
      return user;
    } catch (error) {
      if (error instanceof UserNotFoundError) {
        throw new NotFoundException(error.message);
      }
      this.logger.error(`Unexpected error finding user ${id}:`, error);
      throw new InternalServerErrorException('Failed to retrieve user');
    }
  }

  async create(createUserDto: CreateUserDto): Promise<User> {
    try {
      // Validierung
      const existingUser = await this.userRepository.findOne({
        where: { email: createUserDto.email },
      });
      
      if (existingUser) {
        throw new DuplicateEmailError(createUserDto.email);
      }

      const user = this.userRepository.create(createUserDto);
      return await this.userRepository.save(user);
    } catch (error) {
      if (error instanceof DuplicateEmailError) {
        throw new ConflictException(error.message);
      }
      
      this.logger.error('Failed to create user:', error);
      throw new InternalServerErrorException('Failed to create user');
    }
  }
}

9.6.3 Service Testing Strategies

// Service Unit Test Beispiel
describe('UserService', () => {
  let service: UserService;
  let repository: Repository<User>;
  let logger: LoggerService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: getRepositoryToken(User),
          useValue: {
            findOne: jest.fn(),
            create: jest.fn(),
            save: jest.fn(),
            remove: jest.fn(),
          },
        },
        {
          provide: LoggerService,
          useValue: {
            log: jest.fn(),
            error: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<UserService>(UserService);
    repository = module.get<Repository<User>>(getRepositoryToken(User));
    logger = module.get<LoggerService>(LoggerService);
  });

  describe('findById', () => {
    it('should return user when found', async () => {
      const userId = '123';
      const user = { id: userId, email: 'test@example.com' } as User;
      
      jest.spyOn(repository, 'findOne').mockResolvedValue(user);

      const result = await service.findById(userId);

      expect(result).toEqual(user);
      expect(repository.findOne).toHaveBeenCalledWith({ where: { id: userId } });
    });

    it('should throw NotFoundException when user not found', async () => {
      const userId = '123';
      
      jest.spyOn(repository, 'findOne').mockResolvedValue(null);

      await expect(service.findById(userId)).rejects.toThrow(NotFoundException);
    });
  });
});

// Integration Test Beispiel
describe('UserService Integration', () => {
  let service: UserService;
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [
        TypeOrmModule.forRoot({
          type: 'sqlite',
          database: ':memory:',
          entities: [User],
          synchronize: true,
        }),
        TypeOrmModule.forFeature([User]),
      ],
      providers: [UserService, LoggerService],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
    
    service = moduleFixture.get<UserService>(UserService);
  });

  afterAll(async () => {
    await app.close();
  });

  it('should create and retrieve user', async () => {
    const createUserDto = {
      email: 'integration@test.com',
      firstName: 'Integration',
      lastName: 'Test',
    };

    const createdUser = await service.create(createUserDto);
    const retrievedUser = await service.findById(createdUser.id);

    expect(retrievedUser).toEqual(createdUser);
  });
});

Services und Dependency Injection bilden das Fundament robuster NestJS-Anwendungen. Durch die konsequente Anwendung der SOLID-Prinzipien, die Nutzung des mächtigen DI-Systems und die Implementierung bewährter Patterns entstehen wartbare, testbare und skalierbare Backend-Architekturen, die den Anforderungen moderner Anwendungen gerecht werden.