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.
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