- 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.
339 lines
9.3 KiB
Markdown
339 lines
9.3 KiB
Markdown
---
|
|
name: service-layer-patterns
|
|
description: 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:
|
|
|
|
```mermaid
|
|
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:
|
|
|
|
```mermaid
|
|
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:**
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```mermaid
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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
|
|
|
|
- [Feature Service](../../services/iam-service/src/modules/feature/feature.service.ts) - Example service implementation
|
|
- [Repository Pattern](../repository-pattern/SKILL.md) - Repository patterns
|
|
- [Caching Patterns](../caching-patterns/SKILL.md) - Caching in services
|
|
- [Error Handling](../error-handling-patterns/SKILL.md) - Error handling patterns
|