--- name: service-layer-patterns description: Service layer organization and patterns for GoodGo microservices. Use when implementing business logic, organizing service classes, using dependency injection, composing services, or separating concerns between controllers and repositories. --- # Service Layer Patterns ## When to Use This Skill Use this skill when: - Implementing business logic in services - Organizing service layer code - Using dependency injection patterns - Composing multiple services together - Separating concerns between controllers, services, and repositories - Handling business rules and validations - Implementing service composition patterns ## Core Concepts ### Service Layer Responsibilities The service layer: - Contains business logic - Orchestrates repository calls - Validates business rules - Handles cross-cutting concerns (caching, logging) - Coordinates multiple repositories - Independent of HTTP transport layer ### Dependency Injection Services use constructor injection for dependencies: ```typescript export class UserService { constructor( private userRepository: UserRepository, private cacheService: CacheService ) {} } ``` ## Patterns ### Basic Service Pattern ```typescript import { logger } from '@goodgo/logger'; import { userRepository } from '../repositories/user.repository'; import { NotFoundError } from '../errors/http-error'; export class UserService { async getUserById(id: string) { logger.info('Fetching user by ID', { id }); const user = await userRepository.findById(id); if (!user) { throw new NotFoundError('User', { id }); } return user; } } ``` ### Service with Caching ```typescript export class UserService { constructor( private repository: UserRepository, private cacheService: CacheService ) {} async getUserById(id: string) { const cacheKey = cacheService.keys.user(id); // Try cache first const cached = await this.cacheService.get(cacheKey); if (cached) return cached; // Cache miss - fetch from repository const user = await this.repository.findById(id); if (!user) { throw new NotFoundError('User'); } // Cache for 5 minutes await this.cacheService.set(cacheKey, user, 300); return user; } } ``` ### Service Composition Services can depend on other services: ```typescript export class AccessRequestService { constructor( private accessRequestRepository: AccessRequestRepository, private userService: UserService, private rbacService: RBACService ) {} async createRequest(userId: string, data: CreateRequestDto) { // Use other services const user = await this.userService.getUserById(userId); const hasPermission = await this.rbacService.checkPermission(userId, 'CREATE_REQUEST'); if (!hasPermission) { throw new ForbiddenError('Insufficient permissions'); } return await this.accessRequestRepository.create({ ...data, userId }); } } ``` ### Business Logic Validation Services validate business rules: ```typescript export class UserService { async createUser(data: CreateUserInput) { // Business rule: Check if email exists const existing = await this.repository.findByEmail(data.email); if (existing) { throw new ConflictError('User with this email already exists'); } // Business rule: Validate organization if (data.organizationId) { const org = await this.orgRepository.findById(data.organizationId); if (!org) { throw new NotFoundError('Organization'); } } return await this.repository.create(data); } } ``` ### Service Module Pattern Organize services into modules: ```typescript export class FeatureModule { private controller: FeatureController; private service: FeatureService; private router: Router; constructor() { const repository = new FeatureRepository(prisma); this.service = new FeatureService(repository); this.controller = new FeatureController(this.service); this.router = this.createRouter(); } getRouter(): Router { return this.router; } private createRouter(): Router { const router = Router(); router.get('/', asyncHandler(this.controller.findAll.bind(this.controller))); router.post('/', asyncHandler(this.controller.create.bind(this.controller))); return router; } } ``` ## Best Practices 1. **Single Responsibility**: Each service handles one domain area 2. **Dependency Injection**: Use constructor injection for testability 3. **Business Logic Only**: Keep HTTP concerns in controllers 4. **Use Repositories**: Don't access database directly 5. **Error Handling**: Throw appropriate domain errors 6. **Logging**: Log important operations and errors 7. **Caching**: Implement caching in services, not repositories 8. **Composition**: Compose services for complex operations ## Common Mistakes 1. **HTTP in Services**: Using `req`/`res` in services 2. **Direct Database Access**: Accessing Prisma directly instead of repositories 3. **Too Many Responsibilities**: Service doing too many things 4. **No Error Handling**: Not throwing appropriate errors 5. **Business Logic in Controllers**: Moving business logic to controllers ## Resources - [Feature Service](../../services/iam-service/src/modules/feature/feature.service.ts) - Example service implementation - [Repository Pattern](../repository-pattern/SKILL.md) - Repository patterns - [Caching Patterns](../caching-patterns/SKILL.md) - Caching in services - [Error Handling](../error-handling-patterns/SKILL.md) - Error handling patterns