Service Layer Pattern
An architectural layer that defines the application's boundary and coordinates domain logic, transaction management, and cross-cutting concerns through service classes.
Description
The Service Layer pattern establishes a boundary between the presentation layer (controllers, resolvers, route handlers) and the domain layer (entities, value objects, domain services). Service classes—also called application services or use cases—orchestrate the steps needed to fulfill a business operation: load data from repositories, execute domain logic, persist changes, dispatch events, and return results. The service layer is where transaction boundaries are defined, authorization checks are enforced, and cross-cutting concerns like logging and auditing are coordinated.
In a well-structured application, controllers are thin and delegate immediately to service methods. A CreateOrderController would parse the request, call orderService.createOrder(dto), and format the response. The OrderService would validate the DTO, load the customer from the repository, check inventory, apply pricing rules, persist the order, publish an OrderCreated event, and return the result. This separation means the same service can be invoked from HTTP controllers, GraphQL resolvers, CLI commands, or message queue consumers without duplicating business logic.
The Service Layer should not contain domain logic itself—that belongs in the domain entities and domain services. The application service is an orchestrator, not a decision-maker. If business rules start accumulating in service methods (if-else chains, calculations, validation beyond input format), that logic should be pushed down into domain objects. Common mistakes include: putting all logic in services with anemic domain models (the "transaction script" anti-pattern), services calling other services creating circular dependencies, and monolithic services that handle too many operations instead of being decomposed into focused use cases.
Prompt Snippet
Structure the service layer as single-purpose use case classes (CreateOrderUseCase, CancelOrderUseCase) rather than monolithic OrderService classes. Each use case receives repository interfaces and domain services via constructor injection (tsyringe), wraps the operation in a database transaction using a UnitOfWork pattern (Drizzle's db.transaction()), and returns typed result objects using a Result<T, E> discriminated union instead of throwing exceptions. Emit domain events via an injected EventBus after the transaction commits (not inside it) to prevent publishing events for rolled-back operations. Keep use case classes under 50 lines by delegating domain decisions to aggregate root methods.
Tags
Related Terms
Repository Pattern
An abstraction layer that mediates between the domain/business logic and the data persistence layer, providing a collection-like interface for accessing domain objects.
Dependency Injection
A design pattern where objects receive their dependencies from an external source rather than creating them internally, enabling loose coupling and testability.
Middleware Pattern
A pattern where request processing is composed as a chain of functions, each of which can inspect, transform, or short-circuit the request before passing it to the next handler.
MVC Pattern
An architectural pattern that separates an application into three interconnected components: Model (data), View (UI), and Controller (logic).