Files
pos-system/analysis/patterns-comparison.md

18 KiB

Pattern Comparison: Template vs IAM Service

Overview

This document compares implementation patterns between _template (baseline microservice) and iam-service (production implementation) to guide documentation updates.

Module Structure Pattern

Template Pattern (Simple)

src/modules/
├── common/          # Base repository, shared types
├── feature/         # Example CRUD module
├── health/          # Health checks
└── metrics/         # Prometheus metrics

IAM Pattern (Production)

src/modules/
├── common/          # Base repository, shared types (same)
├── feature/         # Example CRUD module (inherited)
├── health/          # Health checks (same)
├── metrics/         # Prometheus metrics (same)
├── auth/            # + Core authentication
├── rbac/            # + Authorization
├── token/           # + Token management
├── session/         # + Session management
├── mfa/             # + Multi-factor auth
├── social/          # + Social OAuth
├── oidc/            # + OpenID Connect
├── identity/        # + Identity management
├── access/          # + Access workflows
└── governance/      # + Compliance & reporting

Documentation Impact:

  • Template docs should focus on foundational patterns
  • IAM docs should show advanced patterns and real-world implementation

Controller Pattern

Template (Basic)

// src/modules/feature/feature.controller.ts
export class FeatureController {
  constructor(private service: FeatureService) {}

  async create(req: Request, res: Response, next: NextFunction) {
    try {
      const dto = CreateFeatureDto.parse(req.body);
      const result = await this.service.create(dto);
      res.status(201).json({ success: true, data: result });
    } catch (error) {
      next(error);
    }
  }
}

IAM (Production with Auth)

// src/modules/identity/identity.controller.ts
export class IdentityController {
  constructor(
    private service: IdentityService,
    private auditService: AuditService
  ) {}

  @RequireAuth()
  @RequirePermission('users', 'create')
  @RateLimit('strict')
  async create(req: Request, res: Response, next: NextFunction) {
    try {
      const dto = CreateUserDto.parse(req.body);
      const result = await this.service.create(dto);
      
      // Audit logging
      await this.auditService.log('USER_CREATED', req.user.id, {
        userId: result.id,
        correlationId: req.correlationId,
      });
      
      res.status(201).json({ success: true, data: result });
    } catch (error) {
      next(error);
    }
  }
}

Pattern Differences:

  1. Decorators: IAM uses decorators for auth, permissions, rate limiting
  2. Audit Logging: IAM logs all operations
  3. Dependency Injection: IAM injects multiple services (Service + AuditService)

Documentation Impact:

  • Template docs: Show basic controller pattern
  • IAM docs: Show auth decorators, audit logging, multi-service injection

Service Pattern

Template (Simple Business Logic)

// src/modules/feature/feature.service.ts
export class FeatureService {
  constructor(private repository: FeatureRepository) {}

  async create(data: CreateFeatureDto) {
    // Business validation
    const existing = await this.repository.findByName(data.name);
    if (existing) {
      throw new ConflictError('Feature already exists');
    }
    
    return await this.repository.create(data);
  }
}

IAM (with Caching & Complex Logic)

// src/modules/rbac/rbac.service.ts
export class RBACService {
  constructor(
    private repository: RBACRepository,
    private cacheService: CacheService,
    private auditService: AuditService
  ) {}

  async checkPermission(
    userId: string,
    resource: string,
    action: string
  ): Promise<boolean> {
    // Try cache first (L1 → L2)
    const cacheKey = `user:${userId}:permissions`;
    const cached = await this.cacheService.get<string[]>(cacheKey);
    
    let permissions: string[];
    if (cached) {
      permissions = cached;
    } else {
      // Cache miss - fetch from DB
      const userRoles = await this.repository.getUserRoles(userId);
      const rolePermissions = await this.repository.getRolePermissions(userRoles);
      const directPermissions = await this.repository.getUserPermissions(userId);
      
      permissions = [...rolePermissions, ...directPermissions];
      
      // Cache for 5 minutes
      await this.cacheService.set(cacheKey, permissions, 300);
    }
    
    // Check permission
    const required = `${resource}:${action}`;
    const hasPermission = permissions.some(p => this.matchesPermission(p, required));
    
    // Audit log
    await this.auditService.log(
      hasPermission ? 'ACCESS_GRANTED' : 'ACCESS_DENIED',
      userId,
      { resource, action }
    );
    
    return hasPermission;
  }

  private matchesPermission(granted: string, required: string): boolean {
    // Handle wildcards: users:*:* matches users:create:org
    const grantedParts = granted.split(':');
    const requiredParts = required.split(':');
    
    return grantedParts.every((part, i) => 
      part === '*' || part === requiredParts[i]
    );
  }
}

Pattern Differences:

  1. Caching: IAM uses multi-layer caching
  2. Complex Logic: Permission matching with wildcards
  3. Audit Logging: Every operation logged
  4. Performance: Cache-first approach for frequently accessed data

Documentation Impact:

  • Template docs: Simple business logic
  • IAM docs: Caching strategies, complex permission logic, audit logging

Repository Pattern

Template (Basic CRUD)

// src/modules/feature/feature.repository.ts
export class FeatureRepository extends BaseRepository<Feature> {
  constructor(prisma: PrismaClient) {
    super(prisma, 'feature');
  }

  async findByName(name: string): Promise<Feature | null> {
    return this.prisma.feature.findUnique({ where: { name } });
  }
}

IAM (with Joins & Complex Queries)

// src/modules/rbac/rbac.repository.ts
export class RBACRepository extends BaseRepository<Role> {
  constructor(prisma: PrismaClient) {
    super(prisma, 'role');
  }

  async getUserRoles(userId: string): Promise<string[]> {
    const userRoles = await this.prisma.userRole.findMany({
      where: {
        userId,
        OR: [
          { expiresAt: null },
          { expiresAt: { gt: new Date() } }
        ]
      },
      include: { role: true }
    });
    
    return userRoles.map(ur => ur.role.name);
  }

  async getRolePermissions(roleNames: string[]): Promise<string[]> {
    const rolePermissions = await this.prisma.rolePermission.findMany({
      where: { role: { name: { in: roleNames } } },
      include: { permission: true }
    });
    
    return rolePermissions.map(rp => rp.permission.code);
  }

  async getUserPermissions(userId: string): Promise<string[]> {
    const userPermissions = await this.prisma.userPermission.findMany({
      where: { userId },
      include: { permission: true }
    });
    
    return userPermissions.map(up => up.permission.code);
  }
}

Pattern Differences:

  1. Complex Joins: IAM uses nested includes
  2. Conditional Logic: Expiration checking
  3. Data Aggregation: Combining data from multiple sources

Documentation Impact:

  • Template docs: Basic CRUD with unique lookups
  • IAM docs: Complex joins, conditional queries, data aggregation

Middleware Pattern

Template (Basic)

// src/middlewares/logger.middleware.ts
export const loggerMiddleware = (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    logger.info('Request completed', {
      method: req.method,
      url: req.url,
      statusCode: res.statusCode,
      duration,
    });
  });
  
  next();
};

IAM (with Correlation IDs & Audit)

// src/middlewares/logger.middleware.ts
export const loggerMiddleware = (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  const start = Date.now();
  const correlationId = req.correlationId || generateCorrelationId();
  
  // Attach to request for downstream use
  req.correlationId = correlationId;
  res.setHeader('x-correlation-id', correlationId);
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    logger.info('Request completed', {
      correlationId,
      method: req.method,
      url: req.url,
      statusCode: res.statusCode,
      duration,
      userId: req.user?.id,
      ipAddress: req.ip,
      userAgent: req.headers['user-agent'],
    });
    
    // Audit logging for sensitive endpoints
    if (req.url.startsWith('/api/v1/auth') || req.url.startsWith('/api/v1/rbac')) {
      auditService.log('API_REQUEST', req.user?.id, {
        method: req.method,
        url: req.url,
        statusCode: res.statusCode,
        correlationId,
      });
    }
  });
  
  next();
};

Pattern Differences:

  1. Correlation IDs: IAM propagates correlation IDs
  2. Enhanced Context: User ID, IP, user agent
  3. Audit Integration: Sensitive endpoints get audit logs

Documentation Impact:

  • Template docs: Basic request logging
  • IAM docs: Distributed tracing, audit integration, correlation IDs

Configuration Pattern

Template (Simple Zod Validation)

// src/config/app.config.ts
import { z } from 'zod';

const envSchema = z.object({
  PORT: z.coerce.number().default(5000),
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  DATABASE_URL: z.string().url(),
  REDIS_URL: z.string().url().optional(),
});

const env = envSchema.parse(process.env);

export const appConfig = {
  port: env.PORT,
  env: env.NODE_ENV,
  database: { url: env.DATABASE_URL },
  redis: { url: env.REDIS_URL },
};

IAM (with Nested Configs & Secrets)

// src/config/app.config.ts
import { z } from 'zod';

const envSchema = z.object({
  // Server
  PORT: z.coerce.number().default(3001),
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  
  // Database
  DATABASE_URL: z.string().url(),
  DATABASE_POOL_SIZE: z.coerce.number().default(10),
  
  // Redis
  REDIS_HOST: z.string().default('localhost'),
  REDIS_PORT: z.coerce.number().default(6379),
  REDIS_PASSWORD: z.string().optional(),
  REDIS_DB: z.coerce.number().default(0),
  
  // JWT
  JWT_SECRET: z.string().min(32),
  JWT_REFRESH_SECRET: z.string().min(32),
  JWT_ACCESS_EXPIRY: z.string().default('15m'),
  JWT_REFRESH_EXPIRY: z.string().default('7d'),
  
  // Encryption
  ENCRYPTION_KEY: z.string().min(32).optional(),
  
  // OAuth
  GOOGLE_CLIENT_ID: z.string().optional(),
  GOOGLE_CLIENT_SECRET: z.string().optional(),
  FACEBOOK_APP_ID: z.string().optional(),
  FACEBOOK_APP_SECRET: z.string().optional(),
  GITHUB_CLIENT_ID: z.string().optional(),
  GITHUB_CLIENT_SECRET: z.string().optional(),
  
  // Feature Flags
  MFA_ENABLED: z.coerce.boolean().default(true),
  SOCIAL_AUTH_ENABLED: z.coerce.boolean().default(true),
  TRACING_ENABLED: z.coerce.boolean().default(false),
});

const env = envSchema.parse(process.env);

export const appConfig = {
  server: {
    port: env.PORT,
    env: env.NODE_ENV,
  },
  database: {
    url: env.DATABASE_URL,
    poolSize: env.DATABASE_POOL_SIZE,
  },
  redis: {
    host: env.REDIS_HOST,
    port: env.REDIS_PORT,
    password: env.REDIS_PASSWORD,
    db: env.REDIS_DB,
  },
  jwt: {
    secret: env.JWT_SECRET,
    refreshSecret: env.JWT_REFRESH_SECRET,
    accessExpiry: env.JWT_ACCESS_EXPIRY,
    refreshExpiry: env.JWT_REFRESH_EXPIRY,
  },
  encryption: {
    key: env.ENCRYPTION_KEY,
  },
  oauth: {
    google: {
      clientId: env.GOOGLE_CLIENT_ID,
      clientSecret: env.GOOGLE_CLIENT_SECRET,
    },
    facebook: {
      appId: env.FACEBOOK_APP_ID,
      appSecret: env.FACEBOOK_APP_SECRET,
    },
    github: {
      clientId: env.GITHUB_CLIENT_ID,
      clientSecret: env.GITHUB_CLIENT_SECRET,
    },
  },
  features: {
    mfa: env.MFA_ENABLED,
    socialAuth: env.SOCIAL_AUTH_ENABLED,
    tracing: env.TRACING_ENABLED,
  },
};

Pattern Differences:

  1. Nested Configuration: IAM groups related configs
  2. Secrets Management: JWT, encryption, OAuth secrets
  3. Feature Flags: Runtime feature toggles
  4. Complex Validation: Min length requirements, optional fields

Documentation Impact: -Template docs: Show basic Zod validation

  • IAM docs: Show nested configs, secrets management, feature flags

Testing Pattern

Template (Simple Unit Test)

// src/modules/feature/__tests__/feature.service.test.ts
describe('FeatureService', () => {
  let service: FeatureService;
  let mockRepository: any;

  beforeEach(() => {
    mockRepository = {
      findByName: jest.fn(),
      create: jest.fn(),
    };
    service = new FeatureService(mockRepository);
  });

  it('should create feature', async () => {
    mockRepository.findByName.mockResolvedValue(null);
    mockRepository.create.mockResolvedValue({ id: '1', name: 'test' });
    
    const result = await service.create({ name: 'test' });
    
    expect(result).toEqual({ id: '1', name: 'test' });
    expect(mockRepository.create).toHaveBeenCalledWith({ name: 'test' });
  });

  it('should throw conflict error if feature exists', async () => {
    mockRepository.findByName.mockResolvedValue({ id: '1', name: 'test' });
    
    await expect(service.create({ name: 'test' }))
      .rejects
      .toThrow(ConflictError);
  });
});

IAM (with Mocking Complete Dependencies)

// src/modules/rbac/__tests__/rbac.service.test.ts
describe('RBACService', () => {
  let service: RBACService;
  let mockRepository: any;
  let mockCacheService: any;
  let mockAuditService: any;

  beforeEach(() => {
    mockRepository = {
      getUserRoles: jest.fn(),
      getRolePermissions: jest.fn(),
      getUserPermissions: jest.fn(),
    };
    mockCacheService = {
      get: jest.fn(),
      set: jest.fn(),
    };
    mockAuditService = {
      log: jest.fn(),
    };
    
    service = new RBACService(
      mockRepository,
      mockCacheService,
      mockAuditService
    );
  });

  describe('checkPermission', () => {
    it('should return true if user has permission (cache hit)', async () => {
      mockCacheService.get.mockResolvedValue(['users:create:*', 'posts:read:*']);
      
      const result = await service.checkPermission('user1', 'users', 'create');
      
      expect(result).toBe(true);
      expect(mockCacheService.get).toHaveBeenCalledWith('user:user1:permissions');
      expect(mockRepository.getUserRoles).not.toHaveBeenCalled(); // Cache hit
      expect(mockAuditService.log).toHaveBeenCalledWith(
        'ACCESS_GRANTED',
        'user1',
        { resource: 'users', action: 'create' }
      );
    });

    it('should return false if user lacks permission (cache miss)', async () => {
      mockCacheService.get.mockResolvedValue(null); // Cache miss
      mockRepository.getUserRoles.mockResolvedValue(['user']);
      mockRepository.getRolePermissions.mockResolvedValue(['posts:read:*']);
      mockRepository.getUserPermissions.mockResolvedValue([]);
      
      const result = await service.checkPermission('user1', 'users', 'delete');
      
      expect(result).toBe(false);
      expect(mockCacheService.set).toHaveBeenCalledWith(
        'user:user1:permissions',
        ['posts:read:*'],
        300
      );
      expect(mockAuditService.log).toHaveBeenCalledWith(
        'ACCESS_DENIED',
        'user1',
        { resource: 'users', action: 'delete' }
      );
    });

    it('should handle wildcard permissions', async () => {
      mockCacheService.get.mockResolvedValue(['users:*:*']);
      
      const result = await service.checkPermission('user1', 'users', 'create');
      
      expect(result).toBe(true);
    });
  });
});

Pattern Differences:

  1. Multiple Dependencies: IAM mocks repository, cache, audit
  2. Cache Behavior: Tests cache hit and cache miss scenarios
  3. Audit Verification: Ensures audit logs are created
  4. Complex Logic: Tests wildcard permission matching

Documentation Impact:

  • Template docs: Simple mocking, basic test cases
  • IAM docs: Complex mocking, cache behavior, audit verification

Summary Table

Pattern Template IAM Docs to Update
Controller Basic try-catch + Auth decorators, audit api-design.md, middleware-patterns.md
Service Simple logic + Caching, complex logic service-layer-patterns.md, caching-patterns.md
Repository Basic CRUD + Complex joins, aggregation repository-pattern.md, database-prisma.md
Middleware Basic logging + Correlation IDs, audit middleware-patterns.md, observability-monitoring.md
Configuration Simple Zod + Nested configs, secrets configuration-management.md
Testing Simple mocks + Multi-dependency mocks testing-patterns.md
Caching None Multi-layer (L1/L2/L3) caching-patterns.md (NEW examples)
Security Helmet only + Zero-trust, encryption security.md (NEW examples)
Authorization None RBAC + ABAC NEW skill docs needed
Audit Logging None Event sourcing observability-monitoring.md

High Priority

  1. Update caching-patterns.md with IAM multi-layer caching
  2. Update security.md with zero-trust, encryption, JWT patterns
  3. Update service-layer-patterns.md with caching integration
  4. Update middleware-patterns.md with correlation IDs, audit
  5. Update testing-patterns.md with complex mocking examples

Medium Priority

  1. Update repository-pattern.md with complex joins
  2. Update api-design.md with auth middleware patterns
  3. Update configuration-management.md with secrets management
  4. Update observability-monitoring.md with audit logging

New Content Needed

  1. RBAC patterns (currently basic in user rules)
  2. Event sourcing (audit logging implementation)
  3. Zero-trust architecture (security validation)

Code Example Sources

All code examples in docs should reference:

  • Template: For foundational patterns
  • IAM Service: For production patterns

Example Mapping:

  • _template/src/modules/feature/feature.service.ts → Basic service pattern
  • iam-service/src/modules/rbac/rbac.service.ts → Advanced service with caching
  • iam-service/src/core/cache/multi-layer-cache.ts → Caching implementation
  • iam-service/src/core/security/zero-trust-validator.ts → Security patterns