Files
pos-system/docs/en/skills/service-layer-patterns.md
Ho Ngoc Hai 9b6c585f57 Enhance documentation structure and improve bilingual support across skills
- Updated skill documentation files to include structured metadata for better organization.
- Enhanced bilingual descriptions and guidelines for clarity in both English and Vietnamese.
- Refined sections on usage, best practices, and related skills to ensure consistency across all documentation.
- Improved formatting and removed outdated references to streamline the documentation experience.
- Added best practices checklists to relevant skills for better usability and adherence to standards.
2026-01-01 07:35:44 +07:00

5.5 KiB

name, description
name description
service-layer-patterns 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:

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

Resources