Service Layer Design
Purpose
Give the application a boundary where a use case is named, its transaction is demarcated,
its authorisation is decided, and its collaborators are orchestrated — and keep everything
else out of it.
Two failure modes bracket this layer. The pass-through service: one method per
repository method without any distinct contract. A transaction, authorization or stable
boundary alone may justify forwarding; inspect those duties before calling it redundant. The god service: the layer becomes where all
logic lives, because it has the transaction, the repositories and the other services, and
it is always the path of least resistance. The second is the more expensive, and it grows
from the first.
What the layer owns
Application service (use case) Domain (model or script)
──────────────────────────────── ─────────────────────────────
transaction demarcation business rules and invariants
authorisation for the use case calculations
loading and saving aggregates state transitions
orchestrating several collaborators validity of a domain object
translating boundary types inward
publishing/collecting events
translating infrastructure failures
A method that does only the left column and delegates the right is a healthy application
service. A method with no left column entries is a pass-through. A method with right-column
entries inline is where the domain went.
Model and target contract
Fowler's Service Layer defines an application boundary and coordinates business responses;
it is not restricted to thin DDD orchestration. The separation above describes a domain-model
style. Transaction Scripts can legitimately hold business logic; identify the selected model
before moving rules. Inspect Java/framework versions, transaction manager, proxy configuration,
security entrypoints and callers. Examples are partial Spring sketches with application types
omitted, not an instruction to upgrade the stack.
Workflow
- Name the use case, not the entity.
PlaceOrder, CancelSubscription,
SettleInvoice. Services named after entities (OrderService) accumulate every
operation that mentions an order, which is how the god service forms.
- Establish the business transaction here by default. Repository-local defaults and
listener/job entrypoints can demarcate their own actual units; verify propagation rather than
treating layer placement as the mechanism
(
enterprise-transactions).
- Decide authorisation here. This is the layer that knows the actor and the intent.
Enforce actor/resource/tenant policy on every entry path, with trusted identity and
policy decisions. Repository predicates or database policies may be essential enforcement,
not optional secondary checks; endpoint-only checks miss non-HTTP callers.
- In domain-model style, delegate decisions and keep orchestration. Load the aggregate, call one method
on it, save. If the service is computing what the aggregate should become, the rule has
moved out of the domain.
- Translate at the edges. Boundary types in, domain types through, infrastructure
exceptions to meaningful failures out. Nothing framework-specific escapes upward or
inward.
- Justify the layer per module. Inspect transaction semantics, authorization, audit,
stable APIs and read consistency before deleting forwarding methods; write count is not
the threshold.
Decision rules
The use case is one repository call, no invariant, no orchestration,
no independent transaction/security/audit/API boundary
→ consider no service under the module's chosen architecture. A controller may call
a bounded read gateway, or make
the operation a Transaction Script and call it what it is.
The use case writes two or more aggregates, or writes and publishes,
or must be atomic across collaborators
→ explicit coordination boundary. A local transaction covers only enlisted
resources; publication/remote effects need an outbox or another outcome protocol.
Logic belongs to the domain but fits no single object — a decision
across two aggregates, an algorithm needing several roots
→ domain service: framework-free, no transaction, no repository
orchestration, expressed in domain types. Rare; check first
that the logic does not belong on an object.
Business logic is accumulating in the application service because that
is where the repositories are
→ inspect whether this is deliberate Transaction Script or misplaced logic
in a domain-model design. Fix inconsistent placement
(domain-logic-organization), not the service.
Two application services need each other
→ extract the shared work downward (a domain service or a domain
method), or make one of them the caller of a smaller
collaborator. Mutual calls between transactional services are
where propagation surprises and cycles come from.
An external caller needs a coarse-grained, network-shaped operation
→ that is a Remote Facade in front of application services,
not a fatter application service (remote-facade-and-dto).
Rules
- A service layer provides a stable use-case boundary for transaction, authorization,
orchestration and protocol-independent invocation. A transaction is a common justification, not
its only defining responsibility and not dependent on a write-count threshold.
- Assess whether the service expresses the use case and its chosen domain-logic style.
Procedural code alone does not establish misplaced logic in a Transaction Script design.
- Do not add a service layer by default. For read paths and single-write CRUD it is
frequently pure indirection. State per module whether it exists and why
(
architecture-decision-making).
- Application services depend on domain types; domain types never depend on application
services. A domain object calling a service is the inversion that ends with the model
unable to be tested alone.
- Domain services are much rarer than their popularity suggests. Before writing one, check
whether the behaviour belongs on one of the objects it operates on — most candidates do,
and the ones that genuinely do not are typically policies over two aggregates.
- Prefer protocol-independent application signatures.
ResponseEntity,
HttpServletRequest couples it to HTTP. Pageable introduces Spring Data coupling but
is not inherently HTTP-only; decide whether that dependency fits the module contract (layering-and-boundaries).
- In default proxy mode, self-invocation does not apply the callee's transaction/cache attributes;
it still runs in the caller's existing context if one exists. Prefer a collaborator or explicit
transaction boundary when semantics differ; AspectJ mode behaves differently. Self-injection is
usually a smell, not the sole possible fix.
- Apply authorization where actor, intent and resource scope are known, using trusted
identity on every caller path. Domain policy may participate; repository/RLS predicates
may enforce isolation. Do not remove one layer's enforcement without proving equivalent
coverage, including jobs and consumers.
- Orchestration that spans a network boundary is not a transaction. A service that writes
locally and calls a remote system needs an explicit outcome for "local committed, remote
failed" — retries, compensation or an outbox — decided at this layer
(
distribution-boundaries).
- Batch and per-item are different units. Per-item transactions isolate failures but add commit
overhead and may violate batch atomicity; chunk transactions balance restartability, lock time and
throughput. A single transaction is appropriate only when bounded size and atomicity justify it
(
architecture-and-performance).
References
- Service boundaries and responsibilities — worked
application service and domain service in Java, where the transaction and authorisation
sit, orchestrating several aggregates, event publication, and the exact division of
labour with the domain. Read when writing or reviewing a use case implementation.
- Anaemic layers and god services — detecting
both failure modes from the code and from the history, the metrics that discriminate,
the incremental fixes for each, and when a thin service layer is genuinely correct. Read
when a service class is under review, or when deciding whether to keep the layer.
Review output
Name the retained/extracted boundary, its actual duties and callers, preserved transaction/
security semantics, validation performed and unresolved evidence. Missing runtime evidence
keeps conclusions conditional; a short forwarding method alone is not a defect.
1---2name: service-layer-design3description: Designing the layer that fronts business logic: what an application service owns (transaction boundary, authorisation, orchestration, translation) and what it must not absorb, the difference between application and domain services, and whether the layer is warranted at all. Use when every service method is a single repository call, when a service has become where all rules accumulate, when two services call each other and transactions nest, when authorisation is spread between controller and repository, or when a facade is added over a facade. Does not cover where the rules belong (domain-logic-organization), transaction semantics (enterprise-transactions), remote API design (remote-facade-and-dto), or layer dependency direction (layering-and-boundaries).4---56# Service Layer Design78## Purpose910Give the application a boundary where a use case is named, its transaction is demarcated,11its authorisation is decided, and its collaborators are orchestrated — and keep everything12else out of it.1314Two failure modes bracket this layer. The **pass-through service**: one method per15repository method without any distinct contract. A transaction, authorization or stable16boundary alone may justify forwarding; inspect those duties before calling it redundant. The **god service**: the layer becomes where all17logic lives, because it has the transaction, the repositories and the other services, and18it is always the path of least resistance. The second is the more expensive, and it grows19from the first.2021## What the layer owns2223```text24Application service (use case) Domain (model or script)25──────────────────────────────── ─────────────────────────────26transaction demarcation business rules and invariants27authorisation for the use case calculations28loading and saving aggregates state transitions29orchestrating several collaborators validity of a domain object30translating boundary types inward31publishing/collecting events32translating infrastructure failures33```3435A method that does only the left column and delegates the right is a healthy application36service. A method with no left column entries is a pass-through. A method with right-column37entries inline is where the domain went.3839## Model and target contract4041Fowler's Service Layer defines an application boundary and coordinates business responses;42it is not restricted to thin DDD orchestration. The separation above describes a domain-model43style. Transaction Scripts can legitimately hold business logic; identify the selected model44before moving rules. Inspect Java/framework versions, transaction manager, proxy configuration,45security entrypoints and callers. Examples are partial Spring sketches with application types46omitted, not an instruction to upgrade the stack.4748## Workflow49501. **Name the use case, not the entity.** `PlaceOrder`, `CancelSubscription`,51 `SettleInvoice`. Services named after entities (`OrderService`) accumulate every52 operation that mentions an order, which is how the god service forms.532. **Establish the business transaction here by default.** Repository-local defaults and54 listener/job entrypoints can demarcate their own actual units; verify propagation rather than55 treating layer placement as the mechanism56 (`enterprise-transactions`).573. **Decide authorisation here.** This is the layer that knows the actor and the intent.58 Enforce actor/resource/tenant policy on every entry path, with trusted identity and59 policy decisions. Repository predicates or database policies may be essential enforcement,60 not optional secondary checks; endpoint-only checks miss non-HTTP callers.614. **In domain-model style, delegate decisions and keep orchestration.** Load the aggregate, call one method62 on it, save. If the service is computing what the aggregate should become, the rule has63 moved out of the domain.645. **Translate at the edges.** Boundary types in, domain types through, infrastructure65 exceptions to meaningful failures out. Nothing framework-specific escapes upward or66 inward.676. **Justify the layer per module.** Inspect transaction semantics, authorization, audit,68 stable APIs and read consistency before deleting forwarding methods; write count is not69 the threshold.7071## Decision rules7273```text74The use case is one repository call, no invariant, no orchestration,75no independent transaction/security/audit/API boundary76 → consider no service under the module's chosen architecture. A controller may call77 a bounded read gateway, or make78 the operation a Transaction Script and call it what it is.7980The use case writes two or more aggregates, or writes and publishes,81or must be atomic across collaborators82 → explicit coordination boundary. A local transaction covers only enlisted83 resources; publication/remote effects need an outbox or another outcome protocol.8485Logic belongs to the domain but fits no single object — a decision86across two aggregates, an algorithm needing several roots87 → domain service: framework-free, no transaction, no repository88 orchestration, expressed in domain types. Rare; check first89 that the logic does not belong on an object.9091Business logic is accumulating in the application service because that92is where the repositories are93 → inspect whether this is deliberate Transaction Script or misplaced logic94 in a domain-model design. Fix inconsistent placement95 (domain-logic-organization), not the service.9697Two application services need each other98 → extract the shared work downward (a domain service or a domain99 method), or make one of them the caller of a smaller100 collaborator. Mutual calls between transactional services are101 where propagation surprises and cycles come from.102103An external caller needs a coarse-grained, network-shaped operation104 → that is a Remote Facade in front of application services,105 not a fatter application service (remote-facade-and-dto).106```107108## Rules109110- A service layer provides a stable use-case boundary for transaction, authorization,111 orchestration and protocol-independent invocation. A transaction is a common justification, not112 its only defining responsibility and not dependent on a write-count threshold.113- Assess whether the service expresses the use case and its chosen domain-logic style.114 Procedural code alone does not establish misplaced logic in a Transaction Script design.115- **Do not add a service layer by default.** For read paths and single-write CRUD it is116 frequently pure indirection. State per module whether it exists and why117 (`architecture-decision-making`).118- Application services depend on domain types; domain types never depend on application119 services. A domain object calling a service is the inversion that ends with the model120 unable to be tested alone.121- Domain services are much rarer than their popularity suggests. Before writing one, check122 whether the behaviour belongs on one of the objects it operates on — most candidates do,123 and the ones that genuinely do not are typically policies over two aggregates.124- Prefer protocol-independent application signatures. `ResponseEntity`,125 `HttpServletRequest` couples it to HTTP. `Pageable` introduces Spring Data coupling but126 is not inherently HTTP-only; decide whether that dependency fits the module contract (`layering-and-boundaries`).127- In default proxy mode, self-invocation does not apply the callee's transaction/cache attributes;128 it still runs in the caller's existing context if one exists. Prefer a collaborator or explicit129 transaction boundary when semantics differ; AspectJ mode behaves differently. Self-injection is130 usually a smell, not the sole possible fix.131- Apply authorization where actor, intent and resource scope are known, using trusted132 identity on every caller path. Domain policy may participate; repository/RLS predicates133 may enforce isolation. Do not remove one layer's enforcement without proving equivalent134 coverage, including jobs and consumers.135- Orchestration that spans a network boundary is not a transaction. A service that writes136 locally and calls a remote system needs an explicit outcome for "local committed, remote137 failed" — retries, compensation or an outbox — decided at this layer138 (`distribution-boundaries`).139- Batch and per-item are different units. Per-item transactions isolate failures but add commit140 overhead and may violate batch atomicity; chunk transactions balance restartability, lock time and141 throughput. A single transaction is appropriate only when bounded size and atomicity justify it142 (`architecture-and-performance`).143144## References145146- [Service boundaries and responsibilities](references/service-boundaries.md) — worked147 application service and domain service in Java, where the transaction and authorisation148 sit, orchestrating several aggregates, event publication, and the exact division of149 labour with the domain. Read when writing or reviewing a use case implementation.150- [Anaemic layers and god services](references/anaemic-and-god-services.md) — detecting151 both failure modes from the code and from the history, the metrics that discriminate,152 the incremental fixes for each, and when a thin service layer is genuinely correct. Read153 when a service class is under review, or when deciding whether to keep the layer.154155## Review output156157Name the retained/extracted boundary, its actual duties and callers, preserved transaction/158security semantics, validation performed and unresolved evidence. Missing runtime evidence159keeps conclusions conditional; a short forwarding method alone is not a defect.