Application services and command orchestration
Application services and command orchestration#
TL;DR#
An application service is a thin orchestration layer — one method per use case — that translates an incoming command into calls against the domain model (fetch an aggregate via its repository, invoke a behavior method, save, publish events) and owns transaction boundaries, but must contain zero business logic itself; the moment an if statement expressing a business rule appears in an application service, that logic has leaked out of the domain model.
The idea#
Every use case a system supports ("place an order," "close a discussion," "transfer funds") needs some piece of code that receives the incoming request, figures out which aggregate(s) are involved, fetches them, calls the right domain method, and persists the result. That's the application service's entire job — and Vernon is emphatic that its job stops exactly there. The most common and costly confusion in tactical DDD is application services quietly accumulating business logic — validation rules, conditional branches expressing policy, calculations — that belongs on the aggregate or in a domain service (implementing-ddd/02) instead. Once that happens, the application service becomes a second, competing home for domain logic, split awkwardly across two layers, and the domain model degrades toward the anemic-model anti-pattern (rich application services orbiting thin, data-only entities) that DDD's tactical patterns exist to prevent.
How it works#
Step 1 — One method per use case, named after the command it handles#
An application service method corresponds to exactly one thing a user or external system asks the system to do — placeOrder(PlaceOrderCommand), closeDiscussion(CloseDiscussionCommand), transferFunds(TransferFundsCommand). This mirrors CQRS's command/query split (implementing-ddd/14) even in systems that don't otherwise adopt full CQRS — commands go through application services; simple reads often bypass them entirely.
Step 2 — The four-step skeleton: fetch, invoke, persist, publish#
Nearly every application service method follows the same shape:
class PlaceOrderService {
@Transactional
void placeOrder(PlaceOrderCommand cmd) {
Order order = orderRepository.findById(cmd.orderId())
.orElseThrow(() -> new OrderNotFoundException(cmd.orderId()));
order.place(); // all business logic lives here
orderRepository.save(order);
eventPublisher.publish(order.pullEvents()); // implementing-ddd/07
}
}
- Fetch the relevant aggregate(s) via their repositories (
implementing-ddd/08). - Invoke a method on the aggregate — this is where every actual business rule executes (validity checks, state transitions, invariant enforcement) — the application service does not re-implement or duplicate that logic, it just calls it.
- Persist the aggregate via its repository.
- Publish any domain events the aggregate raised during the operation (
implementing-ddd/07).
Step 3 — Own the transaction boundary#
The application service is the natural place to define "what counts as one transaction," consistent with the one-aggregate-per-transaction rule from implementing-ddd/04 — a single application service method typically wraps a single transaction around exactly one aggregate's fetch-invoke-persist cycle. When a use case needs a second aggregate updated too, that update either happens in a separate application service method triggered by a domain event (preserving eventual consistency, implementing-ddd/06), or, for read-only cross-aggregate needs, is handled via a query rather than a command.
Step 4 — Keep validation split correctly: structural vs. business#
Application services are the right place for structural/input validation — is the command well-formed, does the referenced ID exist, is the caller authorized — because that's about the request, not the domain. Business-rule validation ("can this order actually be placed given its current state") belongs inside the aggregate's own method, which should throw or reject if the business rule doesn't hold, rather than the application service pre-checking the rule itself and only calling the aggregate method if it thinks the check will pass — that duplicates logic and risks the two checks drifting out of sync.
Worked example — a forum/collaboration tool.
class CloseDiscussionService {
@Transactional
void closeDiscussion(CloseDiscussionCommand cmd) {
if (!cmd.requestedBy().hasRole(Role.MODERATOR)) { // structural: authorization
throw new NotAuthorizedException();
}
Discussion discussion = discussionRepository.findById(cmd.discussionId())
.orElseThrow(() -> new DiscussionNotFoundException(cmd.discussionId()));
discussion.close(); // business rule ("cannot close an already-archived discussion") lives HERE
discussionRepository.save(discussion);
eventPublisher.publish(discussion.pullEvents());
}
}
Note the authorization check (who is allowed to call this at all) sits in the application service, while the domain rule about when a discussion can transition to closed sits inside discussion.close() itself.
Step 5 — Application services vs. domain services — a frequent point of confusion#
An application service orchestrates a use case (infrastructure-adjacent: transactions, security, fetching/persisting) and is not itself part of the ubiquitous language. A domain service (implementing-ddd/02) expresses genuine cross-entity business logic and is named in the ubiquitous language (FundsTransferService). The tell: if the logic would make sense to a domain expert as a business rule, it's a domain service concern called from the application service, not application-service logic itself.
Pros#
- Keeps the domain model the single source of truth for business logic, which is what makes it possible to unit-test business rules directly against aggregates with zero infrastructure (no database, no transaction manager) — application services then only need thin integration-style tests verifying orchestration, not business-rule correctness.
- Gives every use case a clear, discoverable, single entry point — a new team member can find "what happens when an order is placed" by locating one method, rather than hunting for scattered logic across controllers, services, and validators.
- Cleanly separates transaction/infrastructure concerns (which belong at this layer) from business rules (which don't), preventing the anemic-domain-model drift where all the interesting logic ends up in service classes.
Cons#
- Requires ongoing discipline to resist "just add a small
ifhere" pressure under deadlines — a single misplaced business rule in an application service is easy to introduce and, left unchecked, normalizes further leakage until the domain model is hollowed out. - The one-method-per-use-case discipline can lead to a large number of small service classes/methods in a big system, which some teams experience as class-file sprawl compared to a handful of larger, multi-purpose service classes (a stylistic cost, not a correctness one).
- Junior developers unfamiliar with the domain-service/application-service distinction often place logic incorrectly at first; the boundary isn't self-enforcing by the type system in most languages, so it depends on code review and team convention.
Alternatives#
- Fat service layer (transaction script) — put all logic, including business rules, directly in the service method operating on plain data objects, skipping rich domain objects entirely; simpler and faster to write for a generic/supporting subdomain (
implementing-ddd/01) with little real domain complexity, but doesn't scale to a core domain's rule complexity without becoming an unmaintainable tangle of conditionals. - Controller-as-application-service — skip the separate application-service layer and put orchestration logic directly in HTTP controllers/handlers; reduces one layer of indirection for very small systems, but tightly couples use-case orchestration to a specific delivery mechanism (HTTP), making it harder to reuse the same use case from a message consumer or CLI later.
- Command bus / mediator pattern — route commands through a generic dispatcher to handler classes (one handler class per command) rather than grouping several use-case methods into one application service class; achieves the same fetch-invoke-persist-publish discipline with finer-grained classes, popular in CQRS-heavy architectures (
implementing-ddd/14).
When to use it#
For every command/use case in a core or supporting domain — give it exactly one application service method, follow the fetch-invoke-persist-publish skeleton, and keep every actual business decision inside the aggregate or domain service it calls.
When NOT to use it#
For trivial CRUD operations in a generic subdomain with no real business rules to protect (create/read/update/delete a simple lookup-table-like resource), the full ceremony of a dedicated application service class per operation may be unnecessary overhead — a simpler, more direct CRUD handler is fine there, consistent with implementing-ddd/01's distillation guidance.
Key takeaways / mental model#
An application service should read like a table of contents for a use case, not like the story itself — fetch, invoke, persist, publish. The moment you find yourself explaining why something is allowed or not allowed inside a service method, rather than merely what steps it performs, that explanation belongs on the aggregate or domain service being called, not in the orchestration code.
Self-check questions#
- Find (or recall) an application service method that contains an
ifstatement expressing a business rule rather than pure orchestration. Where should that logic move, and what test would you write to confirm the move didn't change behavior? - Explain the difference between structural validation and business-rule validation using a concrete command example, and justify why each belongs where it does.
- Why does "one aggregate per transaction" (
implementing-ddd/04) naturally imply "roughly one aggregate fetch per application service method" in most designs? - A teammate proposes merging
PlaceOrderServiceandCancelOrderServiceinto oneOrderServiceclass with multiple methods, arguing it reduces file sprawl. What do you gain and lose from that consolidation?
References#
- Implementing Domain-Driven Design (Vaughn Vernon), Chapter 4: "Architecture".
- Implementing Domain-Driven Design (Vaughn Vernon), Chapter 14: "Application".