State Views via Projections

Observe system state changes via projected read models.

Problem

State transitions via events represent the system’s source of truth, but this "raw material" doesn’t itself provide a direct view of the system’s current state, only its history; observing the result of a state change requires one to establish and update such views.

Solution

In an event model, so-called read models represent the "outputs" of the system after state transitions have occurred. A common type of read model is the projection, whereby events are consumed to update a durable or transient data model, typically de-normalized to allow for swift information retrieval via specific queries.

Example

Figure 1 depicts a state view slice that is part of a business flow to register new bikes.

p2 1

Figure 1 - State view slice for bike registration

This projection exposes its data to "public" clients, but projections may also be solely for "internal" use.

The state view slice is implemented as a projection in the sections below.

Updating the read model

In listing 1, a component is set up to listen for the BikeRegisteredEvent to add a newly registered bike to a list.

@Component
public class BikeProjection {

    private final List<BikeStatus> bikes = new ArrayList<>();

    @EventHandler
    public void on(BikeRegisteredEvent event) {
        bikes.add(new BikeStatus(event.bikeId()));
    }

    @QueryHandler
    public List<BikeStatus> handle(GetBikesQuery query) {
        return bikes.stream().toList();
    }
}

Listing 1 - Handling a `BikeRegisteredEvent`

The projection also contains a QueryHandler, which returns the bike list in response to a (typed) GetBikesQuery (see below).

Querying the read model

In listing 2, the GetBikesQuery is sent to the aforementioned QueryHandler in order to retrieve the bike list.

public record GetBikesQuery() {}

@RestController
@RequestMapping("/inventory")
public class InventoryController {

    @Autowired
    private QueryGateway queryGateway;

    @GetMapping("/bikes")
    public CompletableFuture<List<BikeStatus>> getBikes() {
        return queryGateway.query(
                new GetBikesQuery(),
                ResponseTypes.multipleInstancesOf(BikeStatus.class)
        );
    }
}

Listing 2 - Sending a query to retrieve bike list