Files
pos-system/docs/en/skills/service-layer-patterns.md
Ho Ngoc Hai 2640b351c3 Enhance documentation with detailed diagrams and structured flows
- Added request/response flow diagrams to api-design and api-gateway-advanced skills for better visualization of processes.
- Introduced configuration loading flow in configuration-management skill to clarify the configuration process.
- Included error propagation flow in error-handling-patterns skill to illustrate error handling across layers.
- Enhanced various skills with additional diagrams to improve understanding of complex concepts.

These updates aim to provide clearer guidance and improve the overall documentation experience for developers.
2026-01-01 23:22:54 +07:00

9.3 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 Architecture

The service layer follows a three-tier architecture pattern, separating concerns between HTTP handling, business logic, and data access:

graph TB
    subgraph "HTTP Layer"
        Controller["Controller<br/>- HTTP request/response<br/>- Input validation<br/>- Status codes"]
    end
    
    subgraph "Business Layer"
        Service["Service<br/>- Business logic<br/>- Business rules<br/>- Orchestration<br/>- Caching<br/>- Logging"]
    end
    
    subgraph "Data Layer"
        Repository["Repository<br/>- Data access<br/>- CRUD operations<br/>- Database queries"]
        Database[("Database<br/>Prisma")]
    end
    
    Controller -->|"Calls"| Service
    Service -->|"Uses"| Repository
    Repository -->|"Queries"| Database
    
    style Controller fill:#e1f5ff
    style Service fill:#fff4e1
    style Repository fill:#e8f5e9
    style Database fill:#f3e5f5

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. The dependency injection flow shows how components are wired together:

graph LR
    subgraph "Module Initialization"
        Prisma[("Prisma Client")]
        Cache[("Cache Service")]
    end
    
    subgraph "Dependency Creation"
        Repo["UserRepository<br/>(prisma)"]
        Service["UserService<br/>(repository, cache)"]
        Controller["UserController<br/>(service)"]
    end
    
    subgraph "Router Setup"
        Router["Router<br/>(controller methods)"]
    end
    
    Prisma -->|"Injected"| Repo
    Cache -->|"Injected"| Service
    Repo -->|"Injected"| Service
    Service -->|"Injected"| Controller
    Controller -->|"Bound to"| Router
    
    style Prisma fill:#f3e5f5
    style Cache fill:#f3e5f5
    style Repo fill:#e8f5e9
    style Service fill:#fff4e1
    style Controller fill:#e1f5ff
    style Router fill:#e1f5ff

Example Implementation:

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

Patterns

Request Flow Through Layers

The following sequence diagram illustrates how a request flows through the Controller → Service → Repository layers:

sequenceDiagram
    participant Client
    participant Controller
    participant Service
    participant Cache
    participant Repository
    participant Database
    
    Client->>Controller: HTTP Request (GET /users/:id)
    Controller->>Controller: Validate input
    Controller->>Service: getUserById(id)
    
    Service->>Cache: Check cache
    alt Cache Hit
        Cache-->>Service: Return cached user
        Service-->>Controller: Return user
    else Cache Miss
        Service->>Repository: findById(id)
        Repository->>Database: Query user
        Database-->>Repository: User data
        Repository-->>Service: User entity
        
        alt User Not Found
            Service-->>Controller: Throw NotFoundError
            Controller-->>Client: 404 Not Found
        else User Found
            Service->>Cache: Set cache (TTL: 5min)
            Service-->>Controller: Return user
            Controller-->>Client: 200 OK + User data
        end
    end

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 to compose complex operations. The following diagram shows service composition:

graph TB
    subgraph "AccessRequestService"
        ARService["AccessRequestService"]
        ARRepo["AccessRequestRepository"]
    end
    
    subgraph "Dependencies"
        UserService["UserService"]
        RBACService["RBACService"]
    end
    
    subgraph "Supporting Services"
        UserRepo["UserRepository"]
        RBACRepo["RBACRepository"]
    end
    
    ARService -->|"Uses"| UserService
    ARService -->|"Uses"| RBACService
    ARService -->|"Uses"| ARRepo
    
    UserService -->|"Uses"| UserRepo
    RBACService -->|"Uses"| RBACRepo
    
    style ARService fill:#fff4e1
    style UserService fill:#fff4e1
    style RBACService fill:#fff4e1
    style ARRepo fill:#e8f5e9
    style UserRepo fill:#e8f5e9
    style RBACRepo fill:#e8f5e9

Implementation Example:

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