20 Caching Strategien

Caching ist eine der effektivsten Methoden zur Verbesserung der Performance von Webanwendungen. NestJS bietet umfassende Unterstützung für verschiedene Caching-Strategien, von einfachem In-Memory-Caching bis hin zu komplexen verteilten Cache-Systemen. Eine durchdachte Caching-Strategie kann die Antwortzeiten drastisch reduzieren und die Serverlast minimieren.

20.1 In-Memory Caching

In-Memory Caching speichert Daten direkt im Arbeitsspeicher der Anwendung und bietet die schnellsten Zugriffzeiten für häufig verwendete Daten.

20.1.1 Grundlegendes Cache Setup

import { Module, CacheModule } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    CacheModule.registerAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        ttl: configService.get('CACHE_TTL', 300), // 5 Minuten Standard-TTL
        max: configService.get('CACHE_MAX_ITEMS', 1000), // Maximale Anzahl Items
        isGlobal: true, // Cache global verfügbar machen
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

20.1.2 Cache Service Implementation

import { Injectable, Inject, CACHE_MANAGER } from '@nestjs/common';
import { Cache } from 'cache-manager';

@Injectable()
export class CacheService {
  constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

  // Basis-Cache-Operationen
  async get<T>(key: string): Promise<T | undefined> {
    return await this.cacheManager.get<T>(key);
  }

  async set<T>(key: string, value: T, ttl?: number): Promise<void> {
    await this.cacheManager.set(key, value, ttl);
  }

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

  async reset(): Promise<void> {
    await this.cacheManager.reset();
  }

  // Erweiterte Cache-Operationen
  async getOrSet<T>(
    key: string, 
    factory: () => Promise<T>, 
    ttl?: number
  ): Promise<T> {
    let value = await this.get<T>(key);
    
    if (value === undefined) {
      value = await factory();
      await this.set(key, value, ttl);
    }
    
    return value;
  }

  // Cache mit automatischer Serialisierung
  async setObject(key: string, value: any, ttl?: number): Promise<void> {
    const serialized = JSON.stringify(value);
    await this.set(key, serialized, ttl);
  }

  async getObject<T>(key: string): Promise<T | undefined> {
    const serialized = await this.get<string>(key);
    return serialized ? JSON.parse(serialized) : undefined;
  }

  // Batch-Operationen
  async mget<T>(keys: string[]): Promise<(T | undefined)[]> {
    const promises = keys.map(key => this.get<T>(key));
    return Promise.all(promises);
  }

  async mset(keyValuePairs: Array<{ key: string; value: any; ttl?: number }>): Promise<void> {
    const promises = keyValuePairs.map(({ key, value, ttl }) => 
      this.set(key, value, ttl)
    );
    await Promise.all(promises);
  }

  // Cache-Statistiken
  async getStats(): Promise<{
    hits: number;
    misses: number;
    keys: number;
    hitRate: number;
  }> {
    // Implementation abhängig vom verwendeten Cache-Store
    const store = this.cacheManager.store as any;
    
    if (store.getStats) {
      return store.getStats();
    }
    
    // Fallback für Stores ohne Statistiken
    return {
      hits: 0,
      misses: 0,
      keys: 0,
      hitRate: 0,
    };
  }
}

20.1.3 Cache Interceptor für automatisches Caching

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { CacheService } from './cache.service';

@Injectable()
export class AutoCacheInterceptor implements NestInterceptor {
  constructor(private cacheService: CacheService) {}

  async intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Promise<Observable<any>> {
    const request = context.switchToHttp().getRequest();
    const cacheKey = this.generateCacheKey(request);

    // Cache-Lookup
    const cachedResult = await this.cacheService.get(cacheKey);
    if (cachedResult !== undefined) {
      return new Observable(observer => {
        observer.next(cachedResult);
        observer.complete();
      });
    }

    // Bei Cache-Miss: Ausführen und Ergebnis cachen
    return next.handle().pipe(
      tap(async (result) => {
        if (result !== undefined && result !== null) {
          await this.cacheService.set(cacheKey, result, 300); // 5 Minuten TTL
        }
      }),
    );
  }

  private generateCacheKey(request: any): string {
    const { method, url, query, body } = request;
    const keyParts = [method, url];

    if (Object.keys(query).length > 0) {
      keyParts.push(JSON.stringify(query));
    }

    if (body && Object.keys(body).length > 0) {
      keyParts.push(JSON.stringify(body));
    }

    return keyParts.join(':');
  }
}

// Verwendung des Interceptors
@Controller('users')
@UseInterceptors(AutoCacheInterceptor)
export class UsersController {
  constructor(private usersService: UsersService) {}

  @Get()
  async getUsers(@Query() query: any) {
    return this.usersService.findAll(query);
  }
}

20.1.4 Conditional Caching mit Decorators

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

export const CACHE_CONFIG_KEY = 'cache_config';

export interface CacheConfig {
  ttl?: number;
  key?: string;
  condition?: (context: ExecutionContext) => boolean;
}

export const Cacheable = (config: CacheConfig = {}) => 
  SetMetadata(CACHE_CONFIG_KEY, config);

// Smart Cache Interceptor mit Konfiguration
@Injectable()
export class SmartCacheInterceptor implements NestInterceptor {
  constructor(
    private cacheService: CacheService,
    private reflector: Reflector,
  ) {}

  async intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Promise<Observable<any>> {
    const cacheConfig = this.reflector.get<CacheConfig>(
      CACHE_CONFIG_KEY,
      context.getHandler(),
    );

    if (!cacheConfig) {
      return next.handle();
    }

    // Conditional Caching
    if (cacheConfig.condition && !cacheConfig.condition(context)) {
      return next.handle();
    }

    const cacheKey = cacheConfig.key || this.generateCacheKey(context);
    const cachedResult = await this.cacheService.get(cacheKey);

    if (cachedResult !== undefined) {
      return new Observable(observer => {
        observer.next(cachedResult);
        observer.complete();
      });
    }

    return next.handle().pipe(
      tap(async (result) => {
        if (result !== undefined && result !== null) {
          await this.cacheService.set(cacheKey, result, cacheConfig.ttl);
        }
      }),
    );
  }

  private generateCacheKey(context: ExecutionContext): string {
    const request = context.switchToHttp().getRequest();
    const handler = context.getHandler().name;
    const className = context.getClass().name;
    
    return `${className}:${handler}:${this.hashRequest(request)}`;
  }

  private hashRequest(request: any): string {
    const { url, query, body } = request;
    const data = { url, query, body };
    return Buffer.from(JSON.stringify(data)).toString('base64');
  }
}

// Verwendung mit Decorator
@Controller('products')
export class ProductsController {
  @Get(':id')
  @Cacheable({ 
    ttl: 600, // 10 Minuten
    condition: (context) => {
      const request = context.switchToHttp().getRequest();
      return !request.query.nocache; // Caching wenn nocache nicht gesetzt
    }
  })
  async getProduct(@Param('id') id: string) {
    return this.productsService.findById(id);
  }

  @Get()
  @Cacheable({ 
    ttl: 300,
    key: 'products:list' // Statischer Cache-Key
  })
  async getProducts() {
    return this.productsService.findAll();
  }
}

20.2 Redis Integration

Redis bietet persistentes, hochperformantes Caching mit erweiterten Features wie Pub/Sub und komplexen Datenstrukturen.

20.2.1 Redis Cache Store Setup

import { Module, CacheModule } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import * as redisStore from 'cache-manager-redis-store';

@Module({
  imports: [
    CacheModule.registerAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => {
        const isRedisEnabled = configService.get('REDIS_ENABLED', 'false') === 'true';
        
        if (isRedisEnabled) {
          return {
            store: redisStore,
            host: configService.get('REDIS_HOST', 'localhost'),
            port: configService.get('REDIS_PORT', 6379),
            password: configService.get('REDIS_PASSWORD'),
            db: configService.get('REDIS_DB', 0),
            ttl: configService.get('CACHE_TTL', 300),
            max: configService.get('CACHE_MAX_ITEMS', 1000),
            retry_strategy: (options: any) => {
              if (options.error && options.error.code === 'ECONNREFUSED') {
                return new Error('Redis server connection refused');
              }
              if (options.total_retry_time > 1000 * 60 * 60) {
                return new Error('Retry time exhausted');
              }
              if (options.attempt > 10) {
                return undefined;
              }
              return Math.min(options.attempt * 100, 3000);
            },
          };
        } else {
          // Fallback zu In-Memory Cache
          return {
            ttl: configService.get('CACHE_TTL', 300),
            max: configService.get('CACHE_MAX_ITEMS', 1000),
          };
        }
      },
      inject: [ConfigService],
      isGlobal: true,
    }),
  ],
})
export class CacheConfigModule {}

20.2.2 Redis Service mit erweiterten Features

import { Injectable, Inject, CACHE_MANAGER } from '@nestjs/common';
import { Cache } from 'cache-manager';
import Redis from 'ioredis';

@Injectable()
export class RedisService {
  private redis: Redis;

  constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {
    // Direkte Redis-Verbindung für erweiterte Features
    this.redis = new Redis({
      host: process.env.REDIS_HOST || 'localhost',
      port: parseInt(process.env.REDIS_PORT || '6379'),
      password: process.env.REDIS_PASSWORD,
      retryDelayOnFailover: 100,
      maxRetriesPerRequest: 3,
    });
  }

  // Basis Cache-Operationen über Cache Manager
  async get<T>(key: string): Promise<T | undefined> {
    return await this.cacheManager.get<T>(key);
  }

  async set<T>(key: string, value: T, ttl?: number): Promise<void> {
    await this.cacheManager.set(key, value, ttl);
  }

  // Redis-spezifische Operationen
  async setWithExpiry(key: string, value: any, seconds: number): Promise<void> {
    await this.redis.setex(key, seconds, JSON.stringify(value));
  }

  async increment(key: string, amount: number = 1): Promise<number> {
    return await this.redis.incrby(key, amount);
  }

  async decrement(key: string, amount: number = 1): Promise<number> {
    return await this.redis.decrby(key, amount);
  }

  // Hash-Operationen
  async hset(key: string, field: string, value: any): Promise<void> {
    await this.redis.hset(key, field, JSON.stringify(value));
  }

  async hget<T>(key: string, field: string): Promise<T | undefined> {
    const value = await this.redis.hget(key, field);
    return value ? JSON.parse(value) : undefined;
  }

  async hgetall<T>(key: string): Promise<Record<string, T>> {
    const hash = await this.redis.hgetall(key);
    const result: Record<string, T> = {};
    
    for (const [field, value] of Object.entries(hash)) {
      result[field] = JSON.parse(value);
    }
    
    return result;
  }

  // Set-Operationen
  async sadd(key: string, ...members: string[]): Promise<number> {
    return await this.redis.sadd(key, ...members);
  }

  async smembers(key: string): Promise<string[]> {
    return await this.redis.smembers(key);
  }

  async sismember(key: string, member: string): Promise<boolean> {
    return (await this.redis.sismember(key, member)) === 1;
  }

  // Pub/Sub für Cache Invalidation
  async publish(channel: string, message: any): Promise<void> {
    await this.redis.publish(channel, JSON.stringify(message));
  }

  subscribe(channel: string, callback: (message: any) => void): void {
    const subscriber = this.redis.duplicate();
    subscriber.subscribe(channel);
    subscriber.on('message', (receivedChannel, message) => {
      if (receivedChannel === channel) {
        try {
          const parsedMessage = JSON.parse(message);
          callback(parsedMessage);
        } catch (error) {
          console.error('Error parsing Redis message:', error);
        }
      }
    });
  }

  // Lua Scripts für atomare Operationen
  async getAndSetIfNotExists(key: string, value: any, ttl: number): Promise<any> {
    const script = `
      local current = redis.call('GET', KEYS[1])
      if current == false then
        redis.call('SETEX', KEYS[1], ARGV[2], ARGV[1])
        return nil
      else
        return current
      end
    `;

    const result = await this.redis.eval(
      script,
      1,
      key,
      JSON.stringify(value),
      ttl.toString()
    );

    return result ? JSON.parse(result as string) : undefined;
  }

  // Pipeline für Batch-Operationen
  async batchSet(operations: Array<{ key: string; value: any; ttl?: number }>): Promise<void> {
    const pipeline = this.redis.pipeline();

    operations.forEach(({ key, value, ttl }) => {
      if (ttl) {
        pipeline.setex(key, ttl, JSON.stringify(value));
      } else {
        pipeline.set(key, JSON.stringify(value));
      }
    });

    await pipeline.exec();
  }

  // Memory-effiziente Schlüssel-Suche
  async findKeys(pattern: string, count: number = 100): Promise<string[]> {
    const keys: string[] = [];
    let cursor = '0';

    do {
      const result = await this.redis.scan(cursor, 'MATCH', pattern, 'COUNT', count);
      cursor = result[0];
      keys.push(...result[1]);
    } while (cursor !== '0' && keys.length < count);

    return keys.slice(0, count);
  }

  // Cache-Statistiken
  async getRedisInfo(): Promise<any> {
    const info = await this.redis.info('memory');
    const stats = await this.redis.info('stats');
    
    return {
      memory: this.parseRedisInfo(info),
      stats: this.parseRedisInfo(stats),
    };
  }

  private parseRedisInfo(info: string): Record<string, any> {
    const result: Record<string, any> = {};
    
    info.split('\r\n').forEach(line => {
      if (line && !line.startsWith('#')) {
        const [key, value] = line.split(':');
        if (key && value) {
          result[key] = isNaN(Number(value)) ? value : Number(value);
        }
      }
    });

    return result;
  }
}

20.2.3 Redis Cluster Support

import { Injectable } from '@nestjs/common';
import Redis, { Cluster } from 'ioredis';

@Injectable()
export class RedisClusterService {
  private cluster: Cluster;

  constructor() {
    this.cluster = new Redis.Cluster([
      { host: process.env.REDIS_NODE1_HOST, port: 6379 },
      { host: process.env.REDIS_NODE2_HOST, port: 6379 },
      { host: process.env.REDIS_NODE3_HOST, port: 6379 },
    ], {
      redisOptions: {
        password: process.env.REDIS_PASSWORD,
      },
      enableOfflineQueue: false,
      retryDelayOnFailover: 100,
      maxRetriesPerRequest: 3,
    });
  }

  async distributedSet(key: string, value: any, ttl: number): Promise<void> {
    // Cluster-aware Caching mit Konsistenz-Checks
    const serializedValue = JSON.stringify({
      value,
      timestamp: Date.now(),
      ttl,
    });

    await this.cluster.setex(key, ttl, serializedValue);
  }

  async distributedGet<T>(key: string): Promise<T | undefined> {
    const result = await this.cluster.get(key);
    
    if (!result) {
      return undefined;
    }

    try {
      const parsed = JSON.parse(result);
      
      // TTL-Check für zusätzliche Sicherheit
      if (Date.now() - parsed.timestamp > parsed.ttl * 1000) {
        await this.cluster.del(key);
        return undefined;
      }

      return parsed.value;
    } catch (error) {
      console.error('Error parsing cached value:', error);
      return undefined;
    }
  }

  // Consistent Hashing für gleichmäßige Verteilung
  async setWithConsistentHashing(key: string, value: any, ttl: number): Promise<void> {
    const hashedKey = this.hashKey(key);
    await this.distributedSet(hashedKey, value, ttl);
  }

  async getWithConsistentHashing<T>(key: string): Promise<T | undefined> {
    const hashedKey = this.hashKey(key);
    return await this.distributedGet<T>(hashedKey);
  }

  private hashKey(key: string): string {
    // Einfacher Hash für Consistent Hashing
    let hash = 0;
    for (let i = 0; i < key.length; i++) {
      const char = key.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash; // 32-bit Integer
    }
    return `${key}:${Math.abs(hash)}`;
  }
}

20.3 Cache-Aside Pattern

Das Cache-Aside Pattern ist eine bewährte Strategie, bei der die Anwendung den Cache direkt verwaltet und bei Cache-Misses die Daten aus der primären Datenquelle lädt.

20.3.1 Service mit Cache-Aside Implementation

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CacheService } from '../cache/cache.service';

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

  async findById(id: string): Promise<User | null> {
    const cacheKey = `user:${id}`;
    
    // 1. Cache-Lookup
    const cachedUser = await this.cacheService.get<User>(cacheKey);
    if (cachedUser) {
      return cachedUser;
    }

    // 2. Datenbank-Lookup bei Cache-Miss
    const user = await this.userRepository.findOne({ where: { id } });
    
    // 3. Ergebnis in Cache speichern (auch null-Werte für negative Caching)
    if (user) {
      await this.cacheService.set(cacheKey, user, 600); // 10 Minuten
    } else {
      // Negative Caching mit kürzerer TTL
      await this.cacheService.set(cacheKey, null, 60); // 1 Minute
    }

    return user;
  }

  async findAll(options: { page?: number; limit?: number } = {}): Promise<{
    users: User[];
    total: number;
    page: number;
  }> {
    const { page = 1, limit = 10 } = options;
    const cacheKey = `users:page:${page}:limit:${limit}`;

    // Cache-Lookup
    const cached = await this.cacheService.get<{
      users: User[];
      total: number;
      page: number;
    }>(cacheKey);

    if (cached) {
      return cached;
    }

    // Datenbank-Query
    const [users, total] = await this.userRepository.findAndCount({
      skip: (page - 1) * limit,
      take: limit,
      order: { createdAt: 'DESC' },
    });

    const result = { users, total, page };

    // Cache mit kürzerer TTL für Listen
    await this.cacheService.set(cacheKey, result, 300); // 5 Minuten

    return result;
  }

  async create(userData: Partial<User>): Promise<User> {
    const user = await this.userRepository.save(userData);

    // Cache invalidieren und neuen Benutzer cachen
    await this.invalidateUserListCache();
    await this.cacheService.set(`user:${user.id}`, user, 600);

    return user;
  }

  async update(id: string, userData: Partial<User>): Promise<User | null> {
    const result = await this.userRepository.update(id, userData);
    
    if (result.affected === 0) {
      return null;
    }

    const user = await this.userRepository.findOne({ where: { id } });
    
    if (user) {
      // Cache aktualisieren
      await this.cacheService.set(`user:${id}`, user, 600);
      await this.invalidateUserListCache();
    }

    return user;
  }

  async delete(id: string): Promise<boolean> {
    const result = await this.userRepository.delete(id);
    
    if (result.affected && result.affected > 0) {
      // Cache-Eintrag entfernen
      await this.cacheService.del(`user:${id}`);
      await this.invalidateUserListCache();
      return true;
    }

    return false;
  }

  // Erweiterte Cache-Operationen
  async warmupCache(userIds: string[]): Promise<void> {
    const users = await this.userRepository.findByIds(userIds);
    
    const cacheOperations = users.map(user => ({
      key: `user:${user.id}`,
      value: user,
      ttl: 600,
    }));

    await this.cacheService.mset(cacheOperations);
  }

  async refreshUserCache(id: string): Promise<User | null> {
    // Cache-Eintrag löschen und neu laden
    await this.cacheService.del(`user:${id}`);
    return await this.findById(id);
  }

  private async invalidateUserListCache(): Promise<void> {
    // Alle Listenseiten-Caches invalidieren
    const listKeys = await this.cacheService.findKeys('users:page:*');
    await Promise.all(listKeys.map(key => this.cacheService.del(key)));
  }

  // Conditional Cache Update
  async updateWithCacheCheck(id: string, userData: Partial<User>): Promise<User | null> {
    const cacheKey = `user:${id}`;
    const cachedUser = await this.cacheService.get<User>(cacheKey);

    if (cachedUser) {
      // Prüfen ob Update notwendig ist
      const hasChanges = Object.keys(userData).some(
        key => cachedUser[key] !== userData[key]
      );

      if (!hasChanges) {
        return cachedUser; // Keine Änderungen, Cache-Wert zurückgeben
      }
    }

    // Update durchführen
    return await this.update(id, userData);
  }
}

20.3.2 Generic Cache-Aside Repository

import { Injectable } from '@nestjs/common';
import { Repository, DeepPartial, FindManyOptions } from 'typeorm';
import { CacheService } from '../cache/cache.service';

export abstract class CachedRepository<T extends { id: string | number }> {
  protected abstract readonly entityName: string;
  protected abstract readonly repository: Repository<T>;
  protected abstract readonly cacheService: CacheService;
  protected readonly defaultTtl: number = 600; // 10 Minuten

  async findById(id: string | number): Promise<T | null> {
    const cacheKey = this.getCacheKey('id', id);
    
    const cached = await this.cacheService.get<T>(cacheKey);
    if (cached !== undefined) {
      return cached;
    }

    const entity = await this.repository.findOne({ where: { id } as any });
    
    if (entity) {
      await this.cacheService.set(cacheKey, entity, this.defaultTtl);
    } else {
      await this.cacheService.set(cacheKey, null, 60); // Negative Caching
    }

    return entity;
  }

  async findMany(options: FindManyOptions<T>): Promise<T[]> {
    const cacheKey = this.getCacheKey('query', this.hashOptions(options));
    
    const cached = await this.cacheService.get<T[]>(cacheKey);
    if (cached) {
      return cached;
    }

    const entities = await this.repository.find(options);
    await this.cacheService.set(cacheKey, entities, this.defaultTtl / 2);

    return entities;
  }

  async save(entity: DeepPartial<T>): Promise<T> {
    const savedEntity = await this.repository.save(entity);
    
    // Cache aktualisieren
    const cacheKey = this.getCacheKey('id', savedEntity.id);
    await this.cacheService.set(cacheKey, savedEntity, this.defaultTtl);
    
    // Query-Caches invalidieren
    await this.invalidateQueryCaches();

    return savedEntity;
  }

  async remove(id: string | number): Promise<boolean> {
    const result = await this.repository.delete(id);
    
    if (result.affected && result.affected > 0) {
      await this.cacheService.del(this.getCacheKey('id', id));
      await this.invalidateQueryCaches();
      return true;
    }

    return false;
  }

  protected getCacheKey(type: string, identifier: any): string {
    return `${this.entityName}:${type}:${identifier}`;
  }

  protected hashOptions(options: FindManyOptions<T>): string {
    return Buffer.from(JSON.stringify(options)).toString('base64');
  }

  protected async invalidateQueryCaches(): Promise<void> {
    const queryKeys = await this.cacheService.findKeys(`${this.entityName}:query:*`);
    await Promise.all(queryKeys.map(key => this.cacheService.del(key)));
  }
}

// Konkrete Implementierung
@Injectable()
export class UserCachedRepository extends CachedRepository<User> {
  protected readonly entityName = 'user';

  constructor(
    @InjectRepository(User)
    protected readonly repository: Repository<User>,
    protected readonly cacheService: CacheService,
  ) {
    super();
  }

  // Spezifische Cache-Methoden
  async findByEmail(email: string): Promise<User | null> {
    const cacheKey = this.getCacheKey('email', email);
    
    const cached = await this.cacheService.get<User>(cacheKey);
    if (cached !== undefined) {
      return cached;
    }

    const user = await this.repository.findOne({ where: { email } });
    
    if (user) {
      // Doppeltes Caching: by ID und by Email
      await this.cacheService.set(cacheKey, user, this.defaultTtl);
      await this.cacheService.set(this.getCacheKey('id', user.id), user, this.defaultTtl);
    } else {
      await this.cacheService.set(cacheKey, null, 60);
    }

    return user;
  }
}

20.4 Cache Invalidation

Cache Invalidation ist eine der schwierigsten Aufgaben im Caching und erfordert durchdachte Strategien zur Aufrechterhaltung der Datenkonsistenz.

20.4.1 Time-based Invalidation

import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { CacheService } from './cache.service';

@Injectable()
export class CacheInvalidationService {
  constructor(private cacheService: CacheService) {}

  // TTL-basierte Invalidation (automatisch durch Cache-Store)
  async setWithCustomTtl(key: string, value: any, ttlSeconds: number): Promise<void> {
    await this.cacheService.set(key, value, ttlSeconds);
  }

  // Scheduled Invalidation
  @Cron(CronExpression.EVERY_HOUR)
  async invalidateHourlyCache(): Promise<void> {
    const hourlyKeys = await this.cacheService.findKeys('hourly:*');
    await Promise.all(hourlyKeys.map(key => this.cacheService.del(key)));
    console.log(`Invalidated ${hourlyKeys.length} hourly cache entries`);
  }

  @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
  async invalidateDailyCache(): Promise<void> {
    const dailyKeys = await this.cacheService.findKeys('daily:*');
    await Promise.all(dailyKeys.map(key => this.cacheService.del(key)));
    console.log(`Invalidated ${dailyKeys.length} daily cache entries`);
  }

  // Conditional Invalidation basierend auf Datenalter
  async conditionalInvalidation(): Promise<void> {
    const allKeys = await this.cacheService.findKeys('conditional:*');
    
    for (const key of allKeys) {
      const data = await this.cacheService.get<{
        value: any;
        timestamp: number;
        conditions: { maxAge?: number; maxAccess?: number };
      }>(key);

      if (data && this.shouldInvalidate(data)) {
        await this.cacheService.del(key);
      }
    }
  }

  private shouldInvalidate(data: any): boolean {
    const now = Date.now();
    const age = now - data.timestamp;

    // Altersbasierte Invalidation
    if (data.conditions.maxAge && age > data.conditions.maxAge) {
      return true;
    }

    // Weitere Bedingungen können hier hinzugefügt werden
    return false;
  }

  // LRU-ähnliche Invalidation für Memory Management
  async performLruInvalidation(maxCacheSize: number): Promise<void> {
    const stats = await this.cacheService.getStats();
    
    if (stats.keys > maxCacheSize) {
      const excessKeys = stats.keys - maxCacheSize;
      console.log(`Performing LRU invalidation for ${excessKeys} keys`);
      
      // Implementation würde echte LRU-Logik benötigen
      // Hier vereinfachte Version mit Pattern-based Cleanup
      await this.invalidateOldestEntries(excessKeys);
    }
  }

  private async invalidateOldestEntries(count: number): Promise<void> {
    // Vereinfachte Implementierung - in der Praxis würde man 
    // Access-Timestamps tracken und echte LRU implementieren
    const temporaryKeys = await this.cacheService.findKeys('temp:*');
    const keysToDelete = temporaryKeys.slice(0, count);
    
    await Promise.all(keysToDelete.map(key => this.cacheService.del(key)));
  }
}

20.4.2 Event-driven Invalidation

import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { CacheService } from './cache.service';

// Events für Cache Invalidation
export class CacheInvalidationEvent {
  constructor(
    public readonly pattern: string,
    public readonly reason: string,
  ) {}
}

export class EntityChangedEvent {
  constructor(
    public readonly entityType: string,
    public readonly entityId: string | number,
    public readonly operation: 'create' | 'update' | 'delete',
  ) {}
}

@Injectable()
export class EventDrivenCacheInvalidation {
  constructor(private cacheService: CacheService) {}

  @OnEvent('cache.invalidate')
  async handleCacheInvalidation(event: CacheInvalidationEvent): Promise<void> {
    const keys = await this.cacheService.findKeys(event.pattern);
    await Promise.all(keys.map(key => this.cacheService.del(key)));
    
    console.log(`Invalidated ${keys.length} cache entries for pattern: ${event.pattern}, reason: ${event.reason}`);
  }

  @OnEvent('entity.changed')
  async handleEntityChanged(event: EntityChangedEvent): Promise<void> {
    const { entityType, entityId, operation } = event;

    // Direkte Entity-Caches invalidieren
    await this.cacheService.del(`${entityType}:${entityId}`);
    
    // Verwandte Caches invalidieren
    await this.invalidateRelatedCaches(entityType, entityId, operation);
    
    // Listen-Caches invalidieren
    const listKeys = await this.cacheService.findKeys(`${entityType}:list:*`);
    await Promise.all(listKeys.map(key => this.cacheService.del(key)));
  }

  private async invalidateRelatedCaches(
    entityType: string,
    entityId: string | number,
    operation: string,
  ): Promise<void> {
    // Entity-spezifische Invalidation-Logik
    switch (entityType) {
      case 'user':
        await this.invalidateUserRelatedCaches(entityId);
        break;
      case 'product':
        await this.invalidateProductRelatedCaches(entityId);
        break;
      case 'order':
        await this.invalidateOrderRelatedCaches(entityId);
        break;
    }
  }

  private async invalidateUserRelatedCaches(userId: string | number): Promise<void> {
    // User-spezifische Caches
    const userPatterns = [
      `user:profile:${userId}`,
      `user:permissions:${userId}`,
      `user:orders:${userId}:*`,
      `user:favorites:${userId}:*`,
    ];

    for (const pattern of userPatterns) {
      const keys = await this.cacheService.findKeys(pattern);
      await Promise.all(keys.map(key => this.cacheService.del(key)));
    }
  }

  private async invalidateProductRelatedCaches(productId: string | number): Promise<void> {
    const productPatterns = [
      `product:details:${productId}`,
      `product:reviews:${productId}:*`,
      `product:recommendations:*:${productId}`,
      `category:products:*`, // Alle Kategorie-Listen da sich Produkt geändert hat
    ];

    for (const pattern of productPatterns) {
      const keys = await this.cacheService.findKeys(pattern);
      await Promise.all(keys.map(key => this.cacheService.del(key)));
    }
  }

  private async invalidateOrderRelatedCaches(orderId: string | number): Promise<void> {
    // Order-bezogene Caches würden hier invalidiert werden
    const orderPatterns = [
      `order:${orderId}`,
      `order:status:*`,
      `analytics:orders:*`, // Analytics-Caches
    ];

    for (const pattern of orderPatterns) {
      const keys = await this.cacheService.findKeys(pattern);
      await Promise.all(keys.map(key => this.cacheService.del(key)));
    }
  }
}

// Service für Event-Emission
@Injectable()
export class CacheAwareService {
  constructor(private eventEmitter: EventEmitter2) {}

  async updateUser(userId: string, userData: any): Promise<void> {
    // Business Logic...
    
    // Event für Cache Invalidation emittieren
    this.eventEmitter.emit('entity.changed', new EntityChangedEvent('user', userId, 'update'));
  }

  async deleteProduct(productId: string): Promise<void> {
    // Business Logic...
    
    this.eventEmitter.emit('entity.changed', new EntityChangedEvent('product', productId, 'delete'));
  }

  async clearCachePattern(pattern: string, reason: string): Promise<void> {
    this.eventEmitter.emit('cache.invalidate', new CacheInvalidationEvent(pattern, reason));
  }
}

20.4.3 Tag-based Invalidation

import { Injectable } from '@nestjs/common';
import { CacheService } from './cache.service';

interface TaggedCacheEntry {
  value: any;
  tags: string[];
  timestamp: number;
  ttl?: number;
}

@Injectable()
export class TaggedCacheService {
  constructor(private cacheService: CacheService) {}

  async setWithTags(
    key: string,
    value: any,
    tags: string[],
    ttl?: number,
  ): Promise<void> {
    const taggedEntry: TaggedCacheEntry = {
      value,
      tags,
      timestamp: Date.now(),
      ttl,
    };

    // Cache-Eintrag mit Tags speichern
    await this.cacheService.set(key, taggedEntry, ttl);

    // Tag-Mappings aktualisieren
    await this.updateTagMappings(key, tags);
  }

  async getByKey<T>(key: string): Promise<T | undefined> {
    const entry = await this.cacheService.get<TaggedCacheEntry>(key);
    return entry?.value;
  }

  async invalidateByTags(tags: string[]): Promise<number> {
    let invalidatedCount = 0;

    for (const tag of tags) {
      const tagKey = this.getTagKey(tag);
      const keySet = await this.cacheService.get<string[]>(tagKey) || [];

      // Alle Keys mit diesem Tag invalidieren
      await Promise.all(keySet.map(key => this.cacheService.del(key)));
      
      // Tag-Mapping entfernen
      await this.cacheService.del(tagKey);
      
      invalidatedCount += keySet.length;
    }

    return invalidatedCount;
  }

  async findByTag<T>(tag: string): Promise<Array<{ key: string; value: T }>> {
    const tagKey = this.getTagKey(tag);
    const keySet = await this.cacheService.get<string[]>(tagKey) || [];

    const results: Array<{ key: string; value: T }> = [];

    for (const key of keySet) {
      const entry = await this.cacheService.get<TaggedCacheEntry>(key);
      if (entry) {
        results.push({ key, value: entry.value });
      }
    }

    return results;
  }

  async invalidateByPattern(pattern: string): Promise<number> {
    const keys = await this.cacheService.findKeys(pattern);
    
    // Alle gefundenen Keys invalidieren
    await Promise.all(keys.map(key => this.cacheService.del(key)));
    
    // Tag-Mappings für invalidierte Keys aufräumen
    await this.cleanupTagMappings(keys);

    return keys.length;
  }

  private async updateTagMappings(key: string, tags: string[]): Promise<void> {
    for (const tag of tags) {
      const tagKey = this.getTagKey(tag);
      const existingKeys = await this.cacheService.get<string[]>(tagKey) || [];
      
      if (!existingKeys.includes(key)) {
        existingKeys.push(key);
        await this.cacheService.set(tagKey, existingKeys, 3600); // 1 Stunde TTL für Tag-Mappings
      }
    }
  }

  private async cleanupTagMappings(invalidatedKeys: string[]): Promise<void> {
    // Alle Tag-Mappings durchsuchen und invalidierte Keys entfernen
    const tagKeys = await this.cacheService.findKeys('tag:*');
    
    for (const tagKey of tagKeys) {
      const keySet = await this.cacheService.get<string[]>(tagKey) || [];
      const updatedKeySet = keySet.filter(key => !invalidatedKeys.includes(key));
      
      if (updatedKeySet.length === 0) {
        await this.cacheService.del(tagKey);
      } else if (updatedKeySet.length !== keySet.length) {
        await this.cacheService.set(tagKey, updatedKeySet, 3600);
      }
    }
  }

  private getTagKey(tag: string): string {
    return `tag:${tag}`;
  }

  // Convenience-Methoden für häufige Use Cases
  async cacheUserData(userId: string, data: any, ttl?: number): Promise<void> {
    await this.setWithTags(
      `user:${userId}`,
      data,
      ['user', `user:${userId}`, 'profile'],
      ttl,
    );
  }

  async cacheProductData(productId: string, data: any, categoryId: string, ttl?: number): Promise<void> {
    await this.setWithTags(
      `product:${productId}`,
      data,
      ['product', `product:${productId}`, `category:${categoryId}`, 'catalog'],
      ttl,
    );
  }

  async invalidateUserCaches(userId: string): Promise<number> {
    return await this.invalidateByTags([`user:${userId}`]);
  }

  async invalidateCategoryCaches(categoryId: string): Promise<number> {
    return await this.invalidateByTags([`category:${categoryId}`]);
  }

  async invalidateAllUserCaches(): Promise<number> {
    return await this.invalidateByTags(['user']);
  }
}

// Usage Example
@Injectable()
export class ProductService {
  constructor(
    private taggedCache: TaggedCacheService,
    @InjectRepository(Product) private productRepo: Repository<Product>,
  ) {}

  async getProduct(id: string): Promise<Product | null> {
    // Cache-Lookup
    const cached = await this.taggedCache.getByKey<Product>(`product:${id}`);
    if (cached) {
      return cached;
    }

    // Database-Lookup
    const product = await this.productRepo.findOne({ where: { id } });
    
    if (product) {
      // Mit relevanten Tags cachen
      await this.taggedCache.setWithTags(
        `product:${id}`,
        product,
        ['product', `product:${id}`, `category:${product.categoryId}`],
        600,
      );
    }

    return product;
  }

  async updateProduct(id: string, data: Partial<Product>): Promise<Product> {
    const product = await this.productRepo.save({ id, ...data });
    
    // Cache für dieses Produkt invalidieren
    await this.taggedCache.invalidateByTags([`product:${id}`]);
    
    // Falls Kategorie geändert wurde, auch Kategorie-Caches invalidieren
    if (data.categoryId) {
      await this.taggedCache.invalidateByTags([`category:${data.categoryId}`]);
    }

    return product;
  }
}

20.5 Distributed Caching

Distributed Caching ermöglicht es, Cache-Daten über mehrere Server-Instanzen hinweg zu teilen und zu synchronisieren.

20.5.1 Multi-Instance Cache Synchronization

import { Injectable } from '@nestjs/common';
import { RedisService } from './redis.service';

@Injectable()
export class DistributedCacheService {
  private readonly instanceId: string;
  private readonly syncChannel = 'cache:sync';

  constructor(private redisService: RedisService) {
    this.instanceId = `instance:${process.pid}:${Date.now()}`;
    this.setupSynchronization();
  }

  private setupSynchronization(): void {
    // Cache-Synchronization zwischen Instanzen
    this.redisService.subscribe(this.syncChannel, (message) => {
      this.handleSyncMessage(message);
    });
  }

  async set(key: string, value: any, ttl?: number): Promise<void> {
    // Lokaler Cache und Redis
    await Promise.all([
      this.setLocal(key, value, ttl),
      this.redisService.set(key, value, ttl),
    ]);

    // Andere Instanzen über Änderung informieren
    await this.broadcastChange('set', key, this.instanceId);
  }

  async get<T>(key: string): Promise<T | undefined> {
    // Lokaler Cache zuerst
    let value = await this.getLocal<T>(key);
    
    if (value === undefined) {
      // Redis fallback
      value = await this.redisService.get<T>(key);
      
      if (value !== undefined) {
        // Lokalen Cache aktualisieren
        await this.setLocal(key, value);
      }
    }

    return value;
  }

  async del(key: string): Promise<void> {
    await Promise.all([
      this.deleteLocal(key),
      this.redisService.del(key),
    ]);

    await this.broadcastChange('delete', key, this.instanceId);
  }

  async invalidatePattern(pattern: string): Promise<void> {
    // Lokale Keys finden und löschen
    const localKeys = await this.findLocalKeys(pattern);
    await Promise.all(localKeys.map(key => this.deleteLocal(key)));

    // Redis Keys finden und löschen
    const redisKeys = await this.redisService.findKeys(pattern);
    await Promise.all(redisKeys.map(key => this.redisService.del(key)));

    await this.broadcastChange('invalidatePattern', pattern, this.instanceId);
  }

  private async broadcastChange(
    operation: string,
    key: string,
    sourceInstance: string,
  ): Promise<void> {
    const message = {
      operation,
      key,
      sourceInstance,
      timestamp: Date.now(),
    };

    await this.redisService.publish(this.syncChannel, message);
  }

  private async handleSyncMessage(message: any): Promise<void> {
    const { operation, key, sourceInstance, timestamp } = message;

    // Eigene Änderungen ignorieren
    if (sourceInstance === this.instanceId) {
      return;
    }

    // Veraltete Nachrichten ignorieren (älter als 10 Sekunden)
    if (Date.now() - timestamp > 10000) {
      return;
    }

    switch (operation) {
      case 'set':
        // Wert aus Redis laden und lokal setzen
        const value = await this.redisService.get(key);
        if (value !== undefined) {
          await this.setLocal(key, value);
        }
        break;

      case 'delete':
        await this.deleteLocal(key);
        break;

      case 'invalidatePattern':
        const localKeys = await this.findLocalKeys(key); // key ist hier das Pattern
        await Promise.all(localKeys.map(k => this.deleteLocal(k)));
        break;
    }
  }

  // Lokaler Cache (In-Memory)
  private localCache = new Map<string, { value: any; expiry?: number }>();

  private async setLocal(key: string, value: any, ttl?: number): Promise<void> {
    const expiry = ttl ? Date.now() + (ttl * 1000) : undefined;
    this.localCache.set(key, { value, expiry });
  }

  private async getLocal<T>(key: string): Promise<T | undefined> {
    const entry = this.localCache.get(key);
    
    if (!entry) {
      return undefined;
    }

    // TTL prüfen
    if (entry.expiry && Date.now() > entry.expiry) {
      this.localCache.delete(key);
      return undefined;
    }

    return entry.value;
  }

  private async deleteLocal(key: string): Promise<void> {
    this.localCache.delete(key);
  }

  private async findLocalKeys(pattern: string): Promise<string[]> {
    const regex = new RegExp(pattern.replace(/\*/g, '.*'));
    return Array.from(this.localCache.keys()).filter(key => regex.test(key));
  }

  // Cache-Statistiken für Monitoring
  async getDistributedStats(): Promise<{
    local: { size: number; keys: string[] };
    redis: any;
    instance: string;
  }> {
    return {
      local: {
        size: this.localCache.size,
        keys: Array.from(this.localCache.keys()),
      },
      redis: await this.redisService.getRedisInfo(),
      instance: this.instanceId,
    };
  }
}

20.5.2 Consistency Strategies

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

export enum ConsistencyLevel {
  EVENTUAL = 'eventual',
  STRONG = 'strong',
  WEAK = 'weak',
}

@Injectable()
export class ConsistentCacheService {
  constructor(private distributedCache: DistributedCacheService) {}

  async set(
    key: string,
    value: any,
    ttl?: number,
    consistency: ConsistencyLevel = ConsistencyLevel.EVENTUAL,
  ): Promise<void> {
    switch (consistency) {
      case ConsistencyLevel.STRONG:
        await this.setWithStrongConsistency(key, value, ttl);
        break;
      case ConsistencyLevel.EVENTUAL:
        await this.setWithEventualConsistency(key, value, ttl);
        break;
      case ConsistencyLevel.WEAK:
        await this.setWithWeakConsistency(key, value, ttl);
        break;
    }
  }

  private async setWithStrongConsistency(
    key: string,
    value: any,
    ttl?: number,
  ): Promise<void> {
    // Alle Instanzen müssen bestätigen
    const timestamp = Date.now();
    const versionedValue = {
      value,
      version: timestamp,
      checksum: this.calculateChecksum(value),
    };

    // Distributed Lock für starke Konsistenz
    const lockKey = `lock:${key}`;
    const lockAcquired = await this.acquireDistributedLock(lockKey, 5000);

    if (!lockAcquired) {
      throw new Error(`Failed to acquire lock for key: ${key}`);
    }

    try {
      await this.distributedCache.set(key, versionedValue, ttl);
      await this.waitForConsistency(key, timestamp);
    } finally {
      await this.releaseDistributedLock(lockKey);
    }
  }

  private async setWithEventualConsistency(
    key: string,
    value: any,
    ttl?: number,
  ): Promise<void> {
    // Standard-Verhalten: Write-through ohne Wartezeit
    await this.distributedCache.set(key, value, ttl);
  }

  private async setWithWeakConsistency(
    key: string,
    value: any,
    ttl?: number,
  ): Promise<void> {
    // Fire-and-forget: Keine Garantien
    this.distributedCache.set(key, value, ttl).catch(error => {
      console.warn('Weak consistency cache write failed:', error);
    });
  }

  async get<T>(
    key: string,
    consistency: ConsistencyLevel = ConsistencyLevel.EVENTUAL,
  ): Promise<T | undefined> {
    switch (consistency) {
      case ConsistencyLevel.STRONG:
        return await this.getWithStrongConsistency<T>(key);
      case ConsistencyLevel.EVENTUAL:
      case ConsistencyLevel.WEAK:
        return await this.distributedCache.get<T>(key);
    }
  }

  private async getWithStrongConsistency<T>(key: string): Promise<T | undefined> {
    // Read-through mit Konsistenz-Validierung
    const value = await this.distributedCache.get<{
      value: T;
      version: number;
      checksum: string;
    }>(key);

    if (!value) {
      return undefined;
    }

    // Checksum validieren
    const expectedChecksum = this.calculateChecksum(value.value);
    if (value.checksum !== expectedChecksum) {
      console.warn(`Checksum mismatch for key ${key}, invalidating cache`);
      await this.distributedCache.del(key);
      return undefined;
    }

    return value.value;
  }

  private async acquireDistributedLock(
    lockKey: string,
    timeoutMs: number,
  ): Promise<boolean> {
    const lockValue = `${this.getInstanceId()}:${Date.now()}`;
    const acquired = await this.redisService.getAndSetIfNotExists(
      lockKey,
      lockValue,
      Math.ceil(timeoutMs / 1000),
    );

    return acquired === undefined; // Lock acquired if key didn't exist
  }

  private async releaseDistributedLock(lockKey: string): Promise<void> {
    await this.distributedCache.del(lockKey);
  }

  private async waitForConsistency(key: string, version: number): Promise<void> {
    const maxWaitTime = 1000; // 1 Sekunde
    const startTime = Date.now();

    while (Date.now() - startTime < maxWaitTime) {
      const value = await this.distributedCache.get<{ version: number }>(key);
      
      if (value && value.version >= version) {
        return; // Konsistenz erreicht
      }

      await new Promise(resolve => setTimeout(resolve, 50)); // 50ms warten
    }

    console.warn(`Consistency timeout for key: ${key}`);
  }

  private calculateChecksum(value: any): string {
    const crypto = require('crypto');
    const data = JSON.stringify(value);
    return crypto.createHash('md5').update(data).digest('hex');
  }

  private getInstanceId(): string {
    return `instance:${process.pid}`;
  }

  // Conflict Resolution für konkurrierende Updates
  async setWithConflictResolution(
    key: string,
    value: any,
    resolver: (current: any, new: any) => any,
  ): Promise<void> {
    let attempts = 0;
    const maxAttempts = 3;

    while (attempts < maxAttempts) {
      const current = await this.get(key, ConsistencyLevel.STRONG);
      const resolved = current ? resolver(current, value) : value;

      try {
        await this.setWithStrongConsistency(key, resolved);
        return; // Erfolgreich
      } catch (error) {
        attempts++;
        if (attempts >= maxAttempts) {
          throw new Error(`Failed to resolve conflict for key ${key} after ${maxAttempts} attempts`);
        }
        
        // Exponential backoff
        await new Promise(resolve => 
          setTimeout(resolve, Math.pow(2, attempts) * 100)
        );
      }
    }
  }
}

20.6 HTTP Response Caching

HTTP Response Caching nutzt Browser-Caches und Proxy-Server für optimale Performance.

20.6.1 HTTP Cache Headers

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

export interface CacheHeaders {
  maxAge?: number; // Cache-Control: max-age
  sMaxAge?: number; // Cache-Control: s-maxage
  mustRevalidate?: boolean; // Cache-Control: must-revalidate
  noCache?: boolean; // Cache-Control: no-cache
  noStore?: boolean; // Cache-Control: no-store
  private?: boolean; // Cache-Control: private
  public?: boolean; // Cache-Control: public
  etag?: string; // ETag header
  lastModified?: Date; // Last-Modified header
  vary?: string[]; // Vary header
}

@Injectable()
export class HttpCacheInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const response = context.switchToHttp().getResponse();
    
    return next.handle().pipe(
      tap((data) => {
        if (data && data._cacheHeaders) {
          this.setCacheHeaders(response, data._cacheHeaders);
          delete data._cacheHeaders; // Clean up metadata
        }
      }),
    );
  }

  private setCacheHeaders(response: any, headers: CacheHeaders): void {
    // Cache-Control Header
    const cacheControl: string[] = [];

    if (headers.maxAge !== undefined) {
      cacheControl.push(`max-age=${headers.maxAge}`);
    }

    if (headers.sMaxAge !== undefined) {
      cacheControl.push(`s-maxage=${headers.sMaxAge}`);
    }

    if (headers.mustRevalidate) {
      cacheControl.push('must-revalidate');
    }

    if (headers.noCache) {
      cacheControl.push('no-cache');
    }

    if (headers.noStore) {
      cacheControl.push('no-store');
    }

    if (headers.private) {
      cacheControl.push('private');
    }

    if (headers.public) {
      cacheControl.push('public');
    }

    if (cacheControl.length > 0) {
      response.header('Cache-Control', cacheControl.join(', '));
    }

    // ETag Header
    if (headers.etag) {
      response.header('ETag', `"${headers.etag}"`);
    }

    // Last-Modified Header
    if (headers.lastModified) {
      response.header('Last-Modified', headers.lastModified.toUTCString());
    }

    // Vary Header
    if (headers.vary && headers.vary.length > 0) {
      response.header('Vary', headers.vary.join(', '));
    }
  }
}

// Decorator für einfache HTTP-Cache-Konfiguration
export const HttpCache = (headers: CacheHeaders) => {
  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      const result = originalMethod.apply(this, args);

      // Cache-Headers zu Antwort hinzufügen
      if (result && typeof result === 'object') {
        result._cacheHeaders = headers;
      }

      return result;
    };

    return descriptor;
  };
};

20.6.2 Conditional HTTP Caching

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import * as crypto from 'crypto';

@Injectable()
export class ConditionalCacheInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const response = context.switchToHttp().getResponse();

    return next.handle().pipe(
      map((data) => {
        if (!data) {
          return data;
        }

        // ETag für Response generieren
        const etag = this.generateETag(data);
        const lastModified = this.extractLastModified(data);

        // Client-seitige Cache-Headers prüfen
        const clientETag = request.headers['if-none-match'];
        const clientLastModified = request.headers['if-modified-since'];

        // Conditional GET: Prüfen ob Daten sich geändert haben
        if (this.isNotModified(etag, lastModified, clientETag, clientLastModified)) {
          response.status(304); // Not Modified
          response.header('ETag', `"${etag}"`);
          if (lastModified) {
            response.header('Last-Modified', lastModified.toUTCString());
          }
          return null; // Leere Response für 304
        }

        // Response-Headers für zukünftige Conditional Requests setzen
        response.header('ETag', `"${etag}"`);
        if (lastModified) {
          response.header('Last-Modified', lastModified.toUTCString());
        }

        return data;
      }),
    );
  }

  private generateETag(data: any): string {
    const content = JSON.stringify(data);
    return crypto.createHash('md5').update(content).digest('hex');
  }

  private extractLastModified(data: any): Date | null {
    // Versuche Last-Modified aus Daten zu extrahieren
    if (data.updatedAt) {
      return new Date(data.updatedAt);
    }
    if (data.lastModified) {
      return new Date(data.lastModified);
    }
    if (Array.isArray(data) && data.length > 0) {
      // Bei Arrays: neuestes Update-Datum finden
      const dates = data
        .map(item => item.updatedAt || item.lastModified)
        .filter(date => date)
        .map(date => new Date(date));
      
      return dates.length > 0 ? new Date(Math.max(...dates.map(d => d.getTime()))) : null;
    }
    return null;
  }

  private isNotModified(
    etag: string,
    lastModified: Date | null,
    clientETag?: string,
    clientLastModified?: string,
  ): boolean {
    // ETag-Vergleich
    if (clientETag) {
      const cleanClientETag = clientETag.replace(/"/g, '');
      if (cleanClientETag === etag) {
        return true;
      }
    }

    // Last-Modified-Vergleich
    if (clientLastModified && lastModified) {
      const clientDate = new Date(clientLastModified);
      if (clientDate.getTime() >= lastModified.getTime()) {
        return true;
      }
    }

    return false;
  }
}

// Usage in Controller
@Controller('api/products')
@UseInterceptors(ConditionalCacheInterceptor)
export class ProductsController {
  constructor(private productsService: ProductsService) {}

  @Get(':id')
  @HttpCache({ 
    maxAge: 300, // 5 Minuten
    public: true,
    mustRevalidate: true 
  })
  async getProduct(@Param('id') id: string) {
    return await this.productsService.findById(id);
  }

  @Get()
  @HttpCache({ 
    maxAge: 60, // 1 Minute
    sMaxAge: 300, // 5 Minuten für Proxies
    vary: ['Accept-Encoding', 'Authorization'] 
  })
  async getProducts(@Query() query: any) {
    return await this.productsService.findAll(query);
  }
}

20.6.3 Response Compression und Caching

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import * as zlib from 'zlib';

@Injectable()
export class CompressionCacheInterceptor implements NestInterceptor {
  private readonly compressionThreshold = 1024; // 1KB

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const response = context.switchToHttp().getResponse();

    return next.handle().pipe(
      map(async (data) => {
        if (!data) {
          return data;
        }

        const acceptEncoding = request.headers['accept-encoding'] || '';
        const jsonData = JSON.stringify(data);

        // Nur komprimieren wenn Daten groß genug sind
        if (jsonData.length < this.compressionThreshold) {
          return data;
        }

        // Komprimierung basierend auf Client-Unterstützung
        if (acceptEncoding.includes('gzip')) {
          const compressed = await this.gzipCompress(jsonData);
          response.header('Content-Encoding', 'gzip');
          response.header('Content-Length', compressed.length.toString());
          response.header('Vary', 'Accept-Encoding');
          
          // Cache-freundliche Headers
          response.header('Cache-Control', 'public, max-age=300');
          
          return compressed;
        } else if (acceptEncoding.includes('deflate')) {
          const compressed = await this.deflateCompress(jsonData);
          response.header('Content-Encoding', 'deflate');
          response.header('Content-Length', compressed.length.toString());
          response.header('Vary', 'Accept-Encoding');
          
          response.header('Cache-Control', 'public, max-age=300');
          
          return compressed;
        }

        return data;
      }),
    );
  }

  private async gzipCompress(data: string): Promise<Buffer> {
    return new Promise((resolve, reject) => {
      zlib.gzip(data, (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      });
    });
  }

  private async deflateCompress(data: string): Promise<Buffer> {
    return new Promise((resolve, reject) => {
      zlib.deflate(data, (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      });
    });
  }
}

// CDN-freundliches Caching
@Injectable()
export class CdnCacheService {
  async setCdnHeaders(response: any, data: any): Promise<void> {
    const cacheKey = this.generateCacheKey(data);
    
    // CDN-spezifische Headers
    response.header('X-Cache-Key', cacheKey);
    response.header('X-Cache-TTL', '300');
    response.header('X-Cache-Tags', this.generateCacheTags(data).join(','));
    
    // Standard HTTP Cache Headers
    response.header('Cache-Control', 'public, max-age=300, s-maxage=3600');
    response.header('Vary', 'Accept-Encoding, Authorization');
    
    // Edge-Side Includes Support
    if (this.supportsESI(data)) {
      response.header('X-ESI-Enabled', 'true');
    }
  }

  private generateCacheKey(data: any): string {
    // Cache-Key basierend auf Dateninhalt
    const crypto = require('crypto');
    const content = JSON.stringify(data);
    return crypto.createHash('sha256').update(content).digest('hex').substring(0, 16);
  }

  private generateCacheTags(data: any): string[] {
    const tags: string[] = [];
    
    if (data.id) {
      tags.push(`id:${data.id}`);
    }
    if (data.type) {
      tags.push(`type:${data.type}`);
    }
    if (data.category) {
      tags.push(`category:${data.category}`);
    }
    
    return tags;
  }

  private supportsESI(data: any): boolean {
    // Prüfen ob Daten für ESI geeignet sind
    return data && typeof data === 'object' && !Array.isArray(data);
  }
}

Caching-Strategien sind entscheidend für die Performance moderner Webanwendungen. NestJS bietet mit seinem flexiblen Cache-System die Möglichkeit, von einfachem In-Memory-Caching bis hin zu komplexen verteilten Cache-Architekturen alles zu implementieren. Die Wahl der richtigen Strategie hängt von den spezifischen Anforderungen wie Konsistenz, Performance und Skalierbarkeit ab. Durch die Kombination verschiedener Caching-Ebenen - von HTTP-Response-Caching über Application-Level-Caching bis hin zu Database-Caching - lassen sich erhebliche Performance-Verbesserungen erzielen.