Skip to content

Use case patterns

A use case is one thing the system does for someone. It sits at the application boundary: outside it is delivery mechanism — HTTP, a queue consumer, a scheduled job — and inside it is the domain.

This is the smallest catalogue here and the most load-bearing, because the use case is where three other things attach: the acceptance test, the transaction, and the authorisation check.

inbound adapter → use case → domain
outbound ports

A use case class per operation, with one public method. Not a UserService with eleven methods sharing six dependencies, four of which any given method does not use — that class is a namespace, and its tests need every dependency stubbed whatever they exercise.

Command / query separation. A use case either changes something and returns little, or changes nothing and returns a projection. Mixing the two produces operations that cannot be cached, cannot be retried, and cannot be reasoned about.

Input and output boundary. The use case takes a request object it defines and returns a response object it defines — neither is an HTTP type and neither is a domain entity. That is what stops the delivery mechanism leaking inwards and the domain leaking outwards.

Interactor with injected ports. Every external effect goes through a port the use case declares. Stubbing those ports is how the acceptance test runs without a database.

Transaction boundary at the use case. One use case, one transaction. Below it the domain knows nothing about transactions; above it the adapter does not open one. Anything spanning two use cases is a saga, not a transaction.

Authorisation at the boundary. The check belongs where the intent is known — in the use case, which knows what is being attempted, rather than in the controller, which only knows which URL was hit.

Result rather than exception for expected failures. “Payment declined” is an outcome, not an exceptional condition. Modelling it as a return value keeps it in the signature where a caller has to handle it.

This catalogue is where balance is decided, more than any other on this site, because the use case is the class every other class in the application layer hangs off. Balance is not an aesthetic preference: it is the observable property that no class carries noticeably more than its neighbours, and what each one carries matches what its name promises.

The two failures are the same failure at opposite ends:

  • The heavy class. OrderService, eleven methods, six dependencies. Everything touching orders accumulates there because the name is broad enough to admit anything, and nothing is ever obviously out of place.
  • The empty class. OrderMapperHelperImpl, one method, one caller, delegating. Indirection with no decision inside it, added because a layer diagram said something should exist at that level.

The cheapest check is a directory listing. If ls -l application/ shows one file of 400 lines beside nine of 12, the responsibilities were not distributed — they were collected in one place and the rest is packaging. Balance is what SRP looks like when it is applied to a package rather than to a single class.

Balance is only enforceable if each class’s role has a name. The names below are the ones most codebases already half-use, inconsistently; stating them completely is what turns a habit into a vocabulary. A suffix is a promise about what the class does and — more usefully — about what it must not do.

At the boundary:

Stereotype Holds Must not
Controller One inbound transport. Translates an HTTP request into a use case request, and the response back out. Decide anything, hold state, or name a domain type in its public signature.
Listener The same job for what arrives unasked: a message, a domain event, a scheduled tick. Contain the reaction. It delegates to a use case — otherwise the rule is only reachable by publishing a message, including from its tests.
Client The mirror of the controller, outbound. Owns one remote protocol: its endpoints, timeouts, retries and error translation. Let the remote’s payload types travel inward. It returns your model, or a Result.

In the flow:

Stereotype Holds Must not
Handler Exactly one command or event type, and what to do when it arrives. In command-bus codebases this is what a use case is called. Handle two message types. A handler with a switch on message kind is a bus that was inlined.
Processor One step over a stream or a batch: takes an item, produces an item, composes with the next step. Be the name for “does the work”. A processor that cannot be composed with another processor is a use case that was named badly.
Service A stateless domain operation belonging to no single entity — pricing across a basket, a policy that needs three aggregates to answer. Be the default suffix. Service on a class with eleven public methods is a namespace, and it is the single most common shape this catalogue exists to fix.
Strategy One named decision with more than one real implementation, selected at runtime. Exist with one implementation, or with implementations that are each a single expression — that is a function value. See GoF.

Between representations:

Stereotype Holds Must not
Mapper Translation between two representations of the same concept across a boundary: entity ↔ DTO, DTO ↔ use case request. Contain a rule. A mapper with an if on a business condition is a use case hiding in the plumbing, and no test will be looking for it there.
Converter Translation of one value into another: StringMoney, an epoch into an Instant. Total, pure, no I/O. Reach a repository. Purity is what makes it testable with no fixture and reusable everywhere, and it is lost the first time it is allowed a dependency.

The use case itself is deliberately absent from the list. It is named for the operation — CancelOrder, RedeemVoucher — because it is the one class in the application layer whose role is not plumbing.

Two rules make the vocabulary worth having:

A class that deserves two of these names deserves two classes. OrderMapperService is not a compound noun, it is an unresolved decision, and it will grow in whichever of the two directions is least convenient.

A domain name beats a stereotype every time. These suffixes name a role in the plumbing; tactical DDD names the concept. PricingPolicy says more than PriceService, and CancelOrder says more than OrderCommandHandler. Reach for a stereotype when the class genuinely is plumbing — and treat the reach itself as a question worth asking: is this actually the domain, and have I just failed to name it?

The daily habits that keep this vocabulary honest are set out under design hygiene.

Worked example — the namespace, and the use case

Section titled “Worked example — the namespace, and the use case”

The shape almost every codebase starts with. Six dependencies, four responsibilities, and a test for cancellation that has to stub a payment gateway:

A namespace with a Service suffix
@Service
public class OrderService {
private final OrderRepository orders;
private final PaymentGateway payments;
private final ShipmentClient shipments;
private final EmailSender email;
private final PricingClient pricing;
private final AuditLog audit;
public OrderDto place(OrderRequest request) { /* ... */ }
public void cancel(Long id, String reason) { /* ... */ }
public void ship(Long id) { /* ... */ }
public List<OrderDto> search(String query, int page) { /* ... */ }
// ...seven more
}

One class per operation, taking only what that operation needs:

application/CancelOrder.java
public class CancelOrder {
private final Orders orders; // the only port it needs
private final DomainEventPublisher events;
public Result<CancelOrderResponse, CancellationRefused> cancel(CancelOrderRequest request) {
Order order = orders.withId(request.orderId())
.orElseThrow(() -> new OrderNotFoundException(request.orderId()));
if (!order.isCancellableBy(request.actor()))
return Result.failure(CancellationRefused.notPermitted(request.actor()));
order.cancel(request.reason()); // the decision lives in the domain
orders.save(order); // one aggregate, one transaction
events.publish(order.releaseEvents());
return Result.success(CancelOrderResponse.of(order.id(), order.status()));
}
}

Four properties fall out, and only the first was the stated goal:

  • The test stubs one port. CancelOrder cannot be affected by the payment gateway, because it cannot reach it.
  • The authorisation check is where the intent is known. isCancellableBy runs against the loaded aggregate, not against a URL pattern in a filter.
  • “Refused” is in the signature. A caller that ignores CancellationRefused does not compile, which is not true of an exception.
  • ls application/ is the feature list. PlaceOrder, CancelOrder, ShipOrder, RedeemVoucher — an inventory of what the system does, in the ubiquitous language, that no controller package has ever provided.

The request and response types are the use case’s own. Returning the Order aggregate directly is the shortcut that ends with an HTTP consumer depending on the shape of the domain model, at which point the boundary exists only in the package names.

The acceptance test attaches to the use case port, so it exercises real business behaviour without a running deployment — which is what makes all-in-one testing affordable. And the list of use case classes in a codebase is a readable inventory of what the system does, which no controller layer has ever provided.

Outline, with the use case shape and the stereotype vocabulary worked through above. The renderings per stack are not written yet; the Spring Boot and Quarkus blueprint repositories already carry this shape in code.