State Transitions via Commands
Use a command to express an intent to change system state.
Problem
In an event-sourced system, a change in system state is represented solely via an event. The requester of this change must be able to provide the required input data that is to be validated and processed by relevant business logic to yield an event, that is the state transition.
Solution
Construct a command message that encapsulates the type of state change to perform and the required input data. Send the command to a target command handler where validation and processing of the command occurs.
A successful outcome produces an event and (typically) no return value; command handling failures are represented by either exceptions to be handled by the sender, or corresponding events; in the latter case, alternate failure paths might have be modeled.
Example
In figure 1 a state change is modeled where a bike renter is "reserving" a bike as part of the business flow for acquiring a bike for a ride.
Figure 1 - State change slice for bike reservation
The possible outcomes of handling a ReserveBikeCommand
are (see figure 2):
-
The bike has been successfully reserved, a state change manifested by
BikeReservedEvent
. -
The bike could not be reserved due to another renter having reserved it previously, resulting in an exception being thrown to be handled by the sender of the command.
The outcomes of a state change slice follow a Given / When / Then format (also called a specification) and include the happy path and potentially one or more failure paths.
Figure 2 - Specification for bike reservation
The yellow "stickies" in the specification signify "current state", which is managed by an aggregate in the implementation of the slice. At the start of this business flow, the renter is selecting a bike that has been registered as available in a prior, separate business flow. |
Figure 2 - Specification for bike reservation
To implement the state change slice for bike reservation, see the sections below.
Sending the command
Listing 1 illustrates how to construct and send the ReserveBikeCommand
; here, an API endpoint receives a client request for bike selection and constructs the command accordingly.
public record RegisterBikeCommand (@TargetAggregateIdentifier String bikeId) {}
@RestController
@RequestMapping("/inventory")
public class RentalController {
@Autowired
private CommandGateway commandGateway;
@PostMapping("/reserveBike")
public CompletableFuture<String> reserveBike(@RequestParam("bikeId") String bikeId) {
return commandGateway.send(new ReserveBikeCommand(bikeId));
}
}
Listing 1 - Sending a `RegisterBikeCommand`
Processing the command and transitioning state
In listing 2, an aggregate is responsible for maintaining rental state for all bikes.
The ReserveBikeCommand
is processed according to existing invariants, one being that the bike to be reserved is actually available (and not already reserved or rented).
If successful, the state transition occurs via the BikeReservedEvent
and the aggregate state is updated.
@Aggregate
public class Rental {
@AggregateMember
private Map<String, RentalState> rentals = new HashMap<>();
@CommandHandler
public void handle(ReserveBikeCommand command) {
if (!rentals.get(command.bikeId()).equals(RentalState.AVAILABLE)) {
throw new IllegalStateException("Bike is not available");
}
apply(new BikeReservedEvent(command.bikeId()));
}
@EventSourcingHandler
public void on(BikeReservedEvent event) {
rentals.put(event.bikeId(), RentalState.RESERVED);
}
}
Listing 2 - Processing a `RegisterBikeCommand`