Back to all terms
S1S2S3
State & Archintermediate

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.

Also known as: Application Service Layer, Use Case Layer, Application Layer

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

architecturedesign-patternsclean-architecturedddseparation-of-concerns