18 KiB
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:
- Decorators: IAM uses decorators for auth, permissions, rate limiting
- Audit Logging: IAM logs all operations
- 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:
- Caching: IAM uses multi-layer caching
- Complex Logic: Permission matching with wildcards
- Audit Logging: Every operation logged
- 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:
- Complex Joins: IAM uses nested includes
- Conditional Logic: Expiration checking
- 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:
- Correlation IDs: IAM propagates correlation IDs
- Enhanced Context: User ID, IP, user agent
- 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:
- Nested Configuration: IAM groups related configs
- Secrets Management: JWT, encryption, OAuth secrets
- Feature Flags: Runtime feature toggles
- 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:
- Multiple Dependencies: IAM mocks repository, cache, audit
- Cache Behavior: Tests cache hit and cache miss scenarios
- Audit Verification: Ensures audit logs are created
- 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 |
Recommended Documentation Updates
High Priority
- Update
caching-patterns.mdwith IAM multi-layer caching - Update
security.mdwith zero-trust, encryption, JWT patterns - Update
service-layer-patterns.mdwith caching integration - Update
middleware-patterns.mdwith correlation IDs, audit - Update
testing-patterns.mdwith complex mocking examples
Medium Priority
- Update
repository-pattern.mdwith complex joins - Update
api-design.mdwith auth middleware patterns - Update
configuration-management.mdwith secrets management - Update
observability-monitoring.mdwith audit logging
New Content Needed
- RBAC patterns (currently basic in user rules)
- Event sourcing (audit logging implementation)
- 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 patterniam-service/src/modules/rbac/rbac.service.ts→ Advanced service with cachingiam-service/src/core/cache/multi-layer-cache.ts→ Caching implementationiam-service/src/core/security/zero-trust-validator.ts→ Security patterns