- 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.
9.3 KiB
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
- 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