- 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.
5.5 KiB
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
- Single Responsibility: Each service handles one domain area
- Dependency Injection: Use constructor injection for testability
- Business Logic Only: Keep HTTP concerns in controllers
- Use Repositories: Don't access database directly
- Error Handling: Throw appropriate domain errors
- Logging: Log important operations and errors
- Caching: Implement caching in services, not repositories
- Composition: Compose services for complex operations
Common Mistakes
- HTTP in Services: Using
req/resin services - Direct Database Access: Accessing Prisma directly instead of repositories
- Too Many Responsibilities: Service doing too many things
- No Error Handling: Not throwing appropriate errors
- Business Logic in Controllers: Moving business logic to controllers
Resources
- Feature Service - Example service implementation
- Repository Pattern - Repository patterns
- Caching Patterns - Caching in services
- Error Handling - Error handling patterns