Application Layer Implementation Rules
Calling the service straight from the endpoint would make this layer unnecessary. The reason for putting a use case in between is the day the unit of business stops matching the unit of the API. Once a single operation spans several domain services, writing that coordination in the endpoint makes the same sequence unreachable from a batch job.
What lives in the application layer is the two kinds of entry point that the outside world reaches, plus the exception they raise.
| Package | Class | Role |
|---|---|---|
application.usecase | {Operation}UseCase | The use case called from an API |
application.job | {JobName}Job | The entry point for scheduled execution |
application.exception | {AppName}Exception | Checked exception representing a failed use case |
Use Cases
A use case is responsible for three things only: converting between DTOs and domain models, calling a domain service, and translating exceptions.
package jp.co.example.foo.application.usecase;
import jp.co.example.foo.application.exception.OrderAppException;
import jp.co.example.foo.domain.exception.OrderServiceException;
import jp.co.example.foo.domain.model.Order;
import jp.co.example.foo.domain.service.OrderService;
import jp.co.example.foo.domain.service.OrderServiceFactory;
import jp.co.example.foo.presentation.response.OrderResponse;
import jp.co.intra_mart.common.platform.log.Logger;
/**
* Use case that retrieves order information.
*/
public class GetOrderUseCase {
private static final Logger LOGGER = Logger.getLogger(GetOrderUseCase.class);
private final OrderService orderService;
public GetOrderUseCase() {
try {
this.orderService = OrderServiceFactory.getInstance().getOrderService();
} catch (final OrderServiceException e) {
throw new RuntimeException("OrderService の初期化に失敗しました", e);
}
}
/**
* Retrieves order information.
* @param orderId the order ID
* @return the order information, or null when there is no match
* @throws OrderAppException when retrieval fails
*/
public OrderResponse execute(final String orderId) throws OrderAppException {
try {
final Order order = orderService.findByOrderId(orderId);
return order != null ? OrderResponse.fromDomainModel(order) : null;
} catch (final OrderServiceException e) {
LOGGER.error("Failed to get order: orderId=" + orderId, e);
throw new OrderAppException("発注情報の取得に失敗しました", e);
}
}
}
The service is obtained from a factory rather than with new.
Writing new StandardOrderService() pins the caller to the default implementation and makes it impossible to slip in a mock during tests.
The factory's getOrderService() throws the checked OrderServiceException, so the constructor replaces it with an unchecked exception.
Entry points are uniformly named execute.
Since both use cases and jobs have one, callers never have to hunt for the entry point.
Keeping It Thin
Deciding business rules in a use case scatters the same rule across several use cases. When "is this editable" is written twice, once for retrieving an order and once for updating it, fixing only one of them makes the behavior diverge. Put the decision in the domain layer, and let the use case do nothing but call it.
The transaction boundary does not go here either.
Writing a SessionTemplate in a use case carries the dependency on im_mirage into the application layer.
The boundary is established by the service and repository implementations, which are covered in Infrastructure Layer Implementation Rules.
Coordinating several domain services in sequence is a use case's job. When those calls have to land in a single transaction, though, provide a service method that covers the whole unit.
Translating Exceptions
A use case replaces the domain exception with an application exception before handing it to the presentation layer.
} catch (final OrderServiceException e) {
LOGGER.error("Failed to get order: orderId=" + orderId, e);
throw new OrderAppException("発注情報の取得に失敗しました", e);
}
OrderAppException is a checked exception.
The presentation layer replaces it with an exception annotated with @Response(code = 422), and the client receives a 422.
An exception without @Response, including an unanticipated RuntimeException, becomes a 500.
The status code for each kind is covered in Presentation Layer Implementation Rules.
Pass the OrderServiceException you caught as the second argument rather than letting it go.
Discarding it here makes it impossible to tell from the stack trace whether the failure came from a business rule or from persistence.
Batch Jobs
A job is the other entry point in this layer.
It extends BaseJob and gives execute the same role it has in a use case.
package jp.co.example.foo.application.job;
/**
* Job that periodically processes outstanding orders.
*/
public class OrderCleanupJob extends BaseJob {
@Override
public JobResult execute() throws JobExecuteException {
final String targetStatus = getParameter("targetStatus");
int processedCount = 0;
// Call the service to process, and count the items processed
return JobResult.success("処理を完了しました。対象件数: " + processedCount);
}
}
Because a job does not go through HTTP, it never passes the Validator in the presentation layer.
The validity of its input is decided by a service in the domain layer.
That is why the validation on the service side cannot be dropped.
Related Documentation
- Domain Layer Implementation Rules: The service interface that the use case calls
- Presentation Layer Implementation Rules: The endpoint that calls the use case
- An Implementation Across Every Layer: The full set from endpoint down to the database
- Overall Architecture: The responsibilities of each layer and the exception hierarchy