Service Layer Implementation Guide - tonglam/letletme_data GitHub Wiki
The service layer acts as an orchestrator between the API layer and domain layer, implementing business use cases while maintaining functional programming principles using fp-ts. This guide demonstrates the implementation patterns using the Events service as a reference.
A service module should follow this structure:
src/services/{service-name}/
├── index.ts # Public API exports
├── types.ts # Service interfaces and types
├── service.ts # Main service implementation
├── cache.ts # Service-level cache implementation
└── workflow.ts # Complex business workflows
Define service interfaces in types.ts
:
// Service interface
export interface EventService {
readonly getEvents: () => TE.TaskEither<ServiceError, readonly Event[]>;
readonly getEvent: (id: EventId) => TE.TaskEither<ServiceError, Event | null>;
readonly getCurrentEvent: () => TE.TaskEither<ServiceError, Event | null>;
readonly getNextEvent: () => TE.TaskEither<ServiceError, Event | null>;
readonly saveEvents: (events: readonly Event[]) => TE.TaskEither<ServiceError, readonly Event[]>;
}
// Service dependencies
export interface EventServiceDependencies {
readonly bootstrapApi: BootstrapApi;
readonly eventCache: EventCache;
readonly eventRepository: EventRepository;
}
Implement the service in service.ts
following these patterns:
- Pure Functions: Each operation should be implemented as a pure function
- Error Handling: Use TaskEither for consistent error handling
- Dependency Injection: Accept dependencies through factory function
- Composition: Use fp-ts pipe and flow for function composition
Example:
const findAllEvents = (
repository: EventRepositoryOperations,
cache: EventCache,
): TE.TaskEither<ServiceError, readonly Event[]> =>
pipe(
cache.getAllEvents(),
TE.mapLeft((error) =>
createServiceIntegrationError({
message: 'Failed to fetch events from cache',
cause: error,
}),
),
TE.chain((cached) =>
cached.length > 0
? TE.right(cached)
: pipe(
repository.findAll(),
TE.mapLeft((error) =>
createServiceOperationError({
message: 'Failed to fetch events from repository',
cause: error,
}),
),
),
),
);
export const createEventService = (
bootstrapApi: BootstrapApi,
repository: EventRepositoryOperations,
): EventService => {
const cache = createEventServiceCache(bootstrapApi);
return {
getEvents: () => findAllEvents(repository, cache),
// ... other operations
};
};
Implement service-level caching in cache.ts
:
- Cache Factory: Create a cache instance with proper configuration
- Data Provider: Implement data provider for cache misses
- Error Handling: Handle cache errors gracefully
- TTL Management: Configure appropriate TTLs for different data types
Example:
export const createEventServiceCache = (bootstrapApi: BootstrapApi): EventCache => {
const redis = createRedisCache<Event>({
keyPrefix: CachePrefix.EVENT,
defaultTTL: DefaultTTL.EVENT,
});
const config: EventCacheConfig = {
keyPrefix: CachePrefix.EVENT,
season: getCurrentSeason(),
};
const dataProvider = createEventDataProvider(bootstrapApi);
return createEventCache(redis, dataProvider, config);
};
Follow these error handling patterns:
-
Error Types:
- ServiceOperationError: For business logic errors
- ServiceIntegrationError: For external service errors
-
Error Creation:
TE.mapLeft((error) =>
createServiceIntegrationError({
message: 'Failed to fetch from cache',
cause: error,
}),
);
-
Error Flow:
- Catch errors at boundaries
- Transform domain errors to service errors
- Provide meaningful error messages
-
Type Safety:
- Use strict TypeScript configuration
- Avoid type assertions
- Define precise interfaces
- Use branded types for IDs
-
Functional Programming:
- Use fp-ts for functional operations
- Maintain immutability
- Compose functions with pipe
- Handle effects with TaskEither
-
Testing:
- Unit test pure functions
- Mock external dependencies
- Test error scenarios
- Verify type safety
-
Performance:
- Implement proper caching
- Use connection pooling
- Batch operations when possible
- Monitor performance metrics
- Factory Pattern:
export const createService = (deps: Dependencies): Service => {
// Initialize resources
return {
// Implement operations
};
};
- Cache-Repository Pattern:
const findData = (cache: Cache, repository: Repository) =>
pipe(
cache.get(),
TE.chain((cached) => (cached ? TE.right(cached) : repository.find())),
);
- Error Transformation:
TE.mapLeft((error) =>
createServiceError({
message: 'Operation failed',
cause: error,
}),
);
-
Metrics to Track:
- Operation latency
- Cache hit rates
- Error rates
- Resource usage
-
Logging:
- Operation start/end
- Error details
- Performance metrics
- Business events
-
Setup:
- Create service directory structure
- Define service types
- Configure dependencies
-
Implementation:
- Implement service operations
- Set up caching
- Handle errors
- Add logging
-
Testing:
- Write unit tests
- Test error scenarios
- Verify type safety
- Measure performance
-
Documentation:
- Document public API
- Explain error handling
- Describe caching strategy
- List dependencies