Skip to main content

Patternintermediate

Application Service

Overview

An application service orchestrates a use case: it loads the aggregates, calls the domain's operations, persists the result, controls the transaction and publishes the events.

What it does not do is decide anything about the business. That is the line, and it is constantly crossed.

Problem

Every use case needs coordination: someone has to fetch the data, invoke the rule, save and deal with the transaction.

If that coordination lives in the entity, the entity comes to know persistence. If it lives in the HTTP controller, the use case is tied to the channel and cannot be triggered by a queue or a terminal.

The application service is the home for that coordination — and it is precisely because it is the point where everything meets that it attracts business rules that should not be there.

Core Concepts

It coordinates, it does not decide

The responsibility is a sequence with no judgement:

CancelOrder.execute(orderId, reason):
order = repository.find(orderId) ← load
order.cancel(reason) ← delegate the decision
repository.save(order) ← persist
events.publish(order.events()) ← publish

The decision of whether the order can be cancelled belongs to the order. The service only asks.

An if deciding something about the business inside the application service is the symptom of the leak.

The leak test

Facing an application service, ask of each conditional: would this decision exist if there were no software?

Checking whether the user has permission, whether the input format is valid, whether the resource exists — coordination.

Checking whether a shipped order can be cancelled, whether the credit limit allows it, whether the waiting period has passed — business, and it belongs to the domain.

It is the transactional boundary

The application service defines where the transaction begins and ends. That makes it responsible for an architectural decision: one aggregate per transaction, per aggregate.

When a use case has to change two aggregates, this is where the decision surfaces — coordinate by event, accept eventual consistency, or recognize that the boundaries are wrong.

It is the use case

In Clean Architecture, the application service corresponds to the use case interactor. In Ports and Adapters, it implements the primary port.

One application service per use case — CancelOrder, ConfirmPayment — is preferable to a service with fifteen methods, for the same cohesion reason that applies anywhere.

When to Use

  • There is a use case with a coordination sequence.
  • The use case has to be triggered through more than one channel.
  • There is a transaction to control.
  • The domain has to be testable without infrastructure.

When Not to Use

When there is no real coordination. A simple query returning data does not need to pass through an application service — it can go straight from a read projection to the controller. See CQRS level 2.

As a mandatory layer out of symmetry. Application services that merely forward to the repository are an anemic layer. See layering.

In generic or supporting subdomains. The separation between the rings rarely pays off outside the core.

When it would accumulate rules. If rules insist on migrating there, the problem is in the domain: an entity or a domain service to host them is missing.

Alternatives

  • A controller calling the domain directly — appropriate in trivial use cases and simple subdomains.
  • A command with a handler — the same idea with different vocabulary. See Command.
  • A direct query — for reads, without going through the domain.

Trade-offs

Application serviceDirect controller
Use case reusable across channelsTied to the channel
Transaction in one placeScattered
Domain testable without infrastructureTests carry the channel
One class per use caseFewer files
Risk of becoming an anemic layerNo extra layer

Failure Modes

Business rules in the service. The dominant mode, and the one that produces the anemic model.

A service with fifteen methods. It lost cohesion; it became a module facade.

A transaction spanning several aggregates. A wrong boundary or unrecognized eventual consistency.

A service that returns entities. The internal model leaks to the channel. It should return a response type of its own.

Anemic layer. It forwards and coordinates nothing.

Common Mistakes

Putting a business if there. Apply the leak test.

Returning the domain entity.

Creating one service per entity instead of per use case.

Injecting everything. A service with eight dependencies is usually doing too much.

Real-World Example

A subscription system had a SubscriptionService with eleven methods and 700 lines.

Inside cancel, there was:

if subscription.status == ACTIVE and daysSinceStart < 7:
fullRefund = true
else if subscription.status == ACTIVE:
proratedRefund = true

The seven-day rule is the statutory right of withdrawal. It is a business decision, and it was in an application service — where no domain test covered it and where the product team could not find it when looking.

The separation moved the rule into Subscription.cancel(currentDate), which returns the kind of refund due. The application service came to merely execute the refund the subscription determined.

Two concrete gains.

When the company started offering thirty days of withdrawal as a commercial differentiator — above the legal minimum — the change was one line in the entity, with the corresponding unit test. Before, it would have required finding the rule among 700 lines of coordination.

And the same cancellation came to apply to the three channels that triggered it — portal, customer service and the automatic delinquency process — which previously had slightly divergent implementations of the same calculation.

The third implementation, the automatic process, still used 5 days. Nobody knew.

Practical Exercise

Pick an application service in your system and list all its conditionals.

For each, apply the test: would this decision exist without software?

The ones that would are business rules in the wrong place.

Interview Questions

  • What should an application service not do?
  • How do you recognize business rules leaking into the application layer?
  • Why one service per use case rather than one per entity?

Further Exploration

  • Vernon, Vaughn. Implementing Domain-Driven Design. Addison-Wesley, 2013.
  • Martin, Robert C. Clean Architecture. Prentice Hall, 2017.
  • Fowler, Martin. AnemicDomainModel, 2003.
Finished reading this document?Your progress is saved in this browser only.