Over the past few months, I have been fully dedicated to developing a software platform for ESG (Environmental, Social, Governance) rating, dealing with all the architectural and domain complexities that come with it.
At the same time, I experienced the greatest joy of my life: the birth of my daughter. I consciously decided to pause the writing of this blog to fully enjoy her first months.
Now, with great enthusiasm, I am resuming sharing thoughts and experiences in software design.
Today’s topic is one of the core pillars of my architecture for this project: Use Cases in Hexagonal Architecture, a pattern that guarantees order, consistency, and maximum code evolvability.
What are Use Cases?
In Hexagonal Architecture, a Use Case is:
“A primary port interface that exposes a business action, orchestrates the application logic, and defines application policies without depending on external technologies or frameworks.“
This approach provides:
- Clear separation between API and logic
- Excellent testability (interfaces are easy to mock)
- Total independence from frameworks or infrastructure
Use Case + Command: the style I prefer
In my recent projects, I have adopted and refined a very practical pattern: Command as an inner class inside the same Use Case interface.
This keeps the contract and the Command DTO in a single, cohesive location, improving readability and cohesion.
public interface SubmitOrderUseCase {
OrderResult handle(SubmitOrderCommand command);
@Value
class SubmitOrderCommand {
@NotNull
UUID customerId;
@NotEmpty
List<@Valid OrderItemDto> items;
public SubmitOrderCommand(UUID customerId, List<OrderItemDto> items) {
this.customerId = customerId;
this.items = items;
ValidatorUtil.validate(this); // immediate validation
}
public CustomerId toCustomerId() {
return new CustomerId(customerId);
}
public List<OrderItem> toOrderItems() {
return items.stream()
.map(OrderItemDto::toDomain)
.toList();
}
}
Validation at construction time
The core of this pattern is ValidatorUtil, a simple utility that centralises Jakarta Validation when the Command is instantiated.
public final class ValidatorUtil {
private static final Validator validator =
Validation.buildDefaultValidatorFactory().getValidator();
private ValidatorUtil() {}
public static <T> void validate(T object) {
Set<ConstraintViolation<T>> violations = validator.validate(object);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
Use Case implementation
The true strength of the pattern lies in the implementation: the Use Case works exclusively with objects that are already valid and consistent.
@Service
@RequiredArgsConstructor
public class SubmitOrderService implements SubmitOrderUseCase {
private final OrderPort orderPort;
@Override
public Order handle(SubmitOrderCommand command) {
var order = Order.create(command.toCustomerId(), command.toOrderItems());
return orderPort.save(order);
}
}
Benefits of this approach
- Maximum cohesion: all data and validation logic are encapsulated inside the Command
- Purity: the Use Case only orchestrates, without any additional control logic
- Single Responsibility Principle (SRP): the Use Case has exactly one responsibility, coordinating the business logic of a specific scenario, without handling validation or data transformation
- Testability: Commands and Use Cases can be tested independently
- Maintainability: changes to business rules or validation only affect the Command
- Clean architecture: no framework dependencies within the core components
Conclusion
Use Cases in Hexagonal Architecture, combined with the self-validating Command pattern at construction time, represent for me the best possible synthesis of:
- architectural rigor
- code clarity
- operational simplicity
This approach has proven to be extremely robust and reliable under the complexity of the ESG project I worked on, confirming once again its effectiveness in building evolvable and maintainable enterprise systems.
Summary flow
Client ➔ SubmitOrderCommand (already validated) ➔ SubmitOrderUseCase ➔ Domain ➔ Repository ➔ Result
Simplicity, safety, maintainability.
Three fundamental values that every modern enterprise system should aim for.
Comments are closed