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.

p1 1

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.

p1 2

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`