Services in the metamodel come in three flavors, and the distinction between them is strict. A domain service carries domain logic that spans multiple entities. An application service orchestrates a use case and delegates to domain objects — it contains no domain logic itself. An infrastructure service is an adapter to an external system, exposed through an interface and consumed by the domain. Mixing these roles is the single most common way a layered architecture decays; keeping them separate is how the domain stays clean.
Domain Service
A Domain Service is a named set of operations that spans multiple entities or aggregates, where assigning ownership to any single one would be arbitrary. When a piece of behavior genuinely belongs to “the domain” rather than to a particular noun, a domain service is the right home.
Two rules define a domain service:
- Operations, not state. A domain service owns 1..n operations — it has no fields, no identity, no lifecycle of its own.
- Bidirectional calling. A domain service may invoke domain objects, and domain objects may invoke a domain service. Unlike an application service, it is genuinely inside the domain layer.
A domain service belongs to exactly one module and may call infrastructure services through their interfaces.
Example:
domainService PricingService {
satisfies [REQ-ORD-012]
belongs to: module pricing
operation quote(cart: Cart, customer: Customer) -> Money
safe: true
idempotent: true
operation applyPromotion(order: Order, code: PromoCode) -> Order
safe: false
idempotent: false
calls PromotionCatalogClient // infrastructure service
}
PricingService.quote cannot naturally belong to Cart alone or to Customer alone — it combines both plus pricing rules. Putting it on either entity would be arbitrary. A domain service captures it cleanly.
Application Service
An Application Service is an orchestrator. It interprets requests from the presentation layer (API controllers, UI handlers, message consumers) and delegates to domain services and domain objects. Application services manage use-case state — the workflow of a single request, transaction boundaries, authorization, logging — but they contain no domain logic.
The distinction is sharp:
- If a piece of logic references a domain rule, it belongs on an entity, a value type, or a domain service.
- If a piece of logic is about coordinating those calls for a specific use case — “load the order, validate the user can modify it, call
Order.cancel, persist, publish the event” — it belongs in an application service.
An application service belongs to exactly one module. It may delegate to domain services and to entities/aggregates, but it is not called by the domain — the dependency flows outward-to-inward only.
Example:
applicationService CancelOrderUseCase {
satisfies [REQ-ORD-020]
belongs to: module placement
operation handle(command: CancelOrder) -> Result<OrderCancelled, OrderRejected>
// 1. Load aggregate via repository
// 2. Check authorization (user owns order)
// 3. Delegate to Order.cancel ← the domain rule lives here
// 4. Persist
// 5. Publish event
delegates to: Order
delegates to: AuditService
}
Notice what this service does not do: it does not decide whether cancellation is allowed, does not compute refund amounts, does not validate state transitions — those are Order’s job. It only orchestrates.
Infrastructure Service
An Infrastructure Service is an adapter that exposes an external system’s capabilities through the domain’s ubiquitous language. It lives in the infrastructure layer of a hexagonal architecture.
Three rules are non-negotiable:
1. Domain calls infrastructure, never the reverse. The domain layer depends on the infrastructure service’s interface, not on its implementation. The dependency arrow always points outward-to-inward.
2. Interface-defined, adapter-implemented. An infrastructure service is described by an interface whose operations are consumed by entity operations or domain service operations. The interface is the contract; an adapter provides the implementation for a real infrastructure component (HTTP client, database, message broker, third-party SDK).
3. ACL pattern is implemented here. When a downstream bounded context uses the Anti-Corruption Layer pattern against an upstream context, that ACL is an infrastructure service. Its interface speaks the downstream ubiquitous language; its adapter translates to/from the upstream model. No upstream type ever crosses the interface.
An infrastructure service belongs to exactly one module and has exactly one interface.
Example:
infrastructureService PaymentGateway {
satisfies [REQ-NFR-001, REQ-PAY-001]
belongs to: module billing
interface PaymentGatewayPort {
operation authorize(card: CardToken, amount: Money) -> AuthorizationResult
operation capture(authId: AuthorizationId) -> CaptureResult
operation refund(captureId: CaptureId, amount: Money) -> RefundResult
}
// Adapter: StripePaymentGatewayAdapter implements PaymentGatewayPort.
// The Stripe SDK types never appear in the domain — only `CardToken`,
// `Money`, and the result types defined in the interface do.
used by: BillingService.chargeCustomer
used by: Order.capturePayment
}
The domain speaks CardToken and Money; the adapter speaks Stripe’s PaymentIntent and Charge. The interface is the translation boundary. When Stripe changes its API, only the adapter moves — the domain is unaffected. That is the entire point.