| name | service-layer-design |
| description | Design the service layer that coordinates business logic, transactions, and cross-cutting concerns between controllers and domain models. Outputs service interfaces, transaction boundaries, and orchestration patterns. |
| argument-hint | ["application architecture","transaction requirements","cross-cutting concerns","team conventions"] |
| allowed-tools | Read, Write |
Service Layer Design
The service layer sits between the presentation layer (HTTP, CLI) and the domain/data layer. It coordinates use cases, defines transaction boundaries, and handles cross-cutting concerns like logging, authorisation, and event publishing. A well-designed service layer makes business operations explicit and testable.
Process
- One method per use case. Each service method represents a complete business operation. Not CRUD — business actions:
placeOrder, processRefund, activateAccount.
- Define transaction boundaries. Each service method is one transaction. If it fails, everything rolls back.
- Coordinate, don't implement. Services orchestrate domain objects and repositories. Business rules live in the domain.
- Keep services thin. If a service method exceeds 20 lines, the domain model needs richer behaviour.
- Test service methods as units. Mock repositories and external services; test the orchestration.
Service Interface Pattern
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
@dataclass
class PlaceOrderCommand:
customer_id: str
items: list
payment_method_id: str
shipping_address: str
@dataclass
class OrderResult:
order_id: str
status: str
total_amount: float
estimated_delivery: str
class OrderService(ABC):
@abstractmethod
async def place_order(self, cmd: PlaceOrderCommand) -> OrderResult: ...
@abstractmethod
async def cancel_order(self, order_id: str, reason: str) -> None: ...
@abstractmethod
async def process_refund(self, order_id: str) -> None: ...
Service Implementation
from core.domain import Order, OrderItem
from core.repositories import OrderRepository, CustomerRepository
from core.events import EventPublisher
from infrastructure.payments import PaymentGateway
class OrderServiceImpl(OrderService):
def __init__(
self,
order_repo: OrderRepository,
customer_repo: CustomerRepository,
payment: PaymentGateway,
events: EventPublisher,
):
self._orders = order_repo
self._customers = customer_repo
self._payment = payment
self._events = events
async def place_order(self, cmd: PlaceOrderCommand) -> OrderResult:
customer = await self._customers.get(cmd.customer_id)
if not customer:
raise CustomerNotFoundError(cmd.customer_id)
if not customer.can_place_orders():
raise CustomerSuspendedError(cmd.customer_id)
order = Order.create(
customer_id=cmd.customer_id,
items=[OrderItem(pid, qty) for pid, qty in cmd.items],
shipping_address=cmd.shipping_address,
)
charge = await self._payment.charge(order.total, cmd.payment_method_id)
charge.success:
PaymentDeclinedError(charge.error)
order.confirm(transaction_id=charge.transaction_id)
._orders.save(order)
._events.publish(, {: order.})
OrderResult(
order_id=order.,
status=order.status,
total_amount=(order.total),
estimated_delivery=order.estimated_delivery_date.isoformat(),
)
Transaction Management
from functools import wraps
def transactional(fn):
@wraps(fn)
async def wrapper(self, *args, **kwargs):
async with self._session.begin():
return await fn(self, *args, **kwargs)
return wrapper
class OrderServiceImpl(OrderService):
@transactional
async def place_order(self, cmd: PlaceOrderCommand) -> OrderResult:
...
class OrderServiceImpl(OrderService):
async def place_order(self, cmd: PlaceOrderCommand) -> OrderResult:
async with self._unit_of_work as uow:
order = Order.create(...)
await uow.orders.save(order)
await uow.commit()
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Fix |
|---|
| CRUD service methods | createOrder, updateOrder — no business meaning | Business verbs: placeOrder, confirmOrder |
| Business logic in service | Domain rules scattered outside domain | Domain objects enforce their own invariants |
| Service calling other services | Tangled dependencies, hard to test | Services coordinate domain + infra only |
| No transaction boundary | Partial writes on failure | One service method = one transaction |
| Returning domain objects | Leaks internals; tight coupling | Return DTOs; map in service layer |
10 Rules
- One service method = one business use case = one transaction.
- Services orchestrate; domain objects implement business rules.
- Service inputs and outputs are DTOs — never expose domain objects across the layer boundary.
- Services depend on interfaces (repositories, gateways) — not concrete implementations.
- Cross-cutting concerns (logging, auth checks) go in middleware or decorators — not in service methods.
- A service method that calls another service method is a smell — consolidate into one method or extract a use case.
- Services are stateless — all state lives in repositories.
- Test service methods by mocking infrastructure — not by hitting the database.
- Error types are domain errors (CustomerSuspended, PaymentDeclined) — not generic exceptions.
- Event publishing happens at the end of the service method — after all state changes succeed.