Files
pos-system/.agent/rules/service-layer-patterns.md

6.6 KiB

trigger
trigger
always_on

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:

export class UserService {
  constructor(
    private userRepository: UserRepository,
    private cacheService: CacheService
  ) {}
}

Patterns

Basic Service Pattern

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

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<User>(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:

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:

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:

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

Quick Reference

Service Responsibilities:

Layer Responsibility Example
Controller HTTP handling, validation Parse request, send response
Service Business logic Validate rules, orchestrate
Repository Data access CRUD operations

Service Template:

export class EntityService {
  constructor(
    private repository: EntityRepository,
    private cache?: CacheService
  ) {}

  async findById(id: string): Promise<Entity> {
    const entity = await this.repository.findById(id);
    if (!entity) throw new NotFoundError('Entity');
    return entity;
  }

  async create(data: CreateDto): Promise<Entity> {
    // Business validation
    await this.validateBusinessRules(data);
    return this.repository.create(data);
  }
}

Dependency Injection Pattern:

// Module setup
const repository = new EntityRepository(prisma);
const service = new EntityService(repository, cacheService);
const controller = new EntityController(service);

Common Patterns:

Pattern When to Use
Caching Frequently accessed data
Composition Complex operations across domains
Validation Business rule enforcement
Logging Audit and debugging

Resources