REFERENCE · Event sourcing · Blackjack Ensemble course
Reference card — print me
PlayerAccountRepository.find()How the repository rebuilds a PlayerAccount aggregate by replaying its stored events — the read side of event sourcing. Pairs with the Repo Map's “two ways state is held”.
find() reads the event rows, turns them back into domain events, and folds them into a fresh account.
find(playerId)
│
▼
eventDtosByPlayer.get(playerId) # Map<PlayerId, List<EventDto>>
│
├─ null ───────────────▶ Optional.empty() # unknown player → no account
│
└─ List<EventDto>
│
▼
mapPlayer(playerId, events)
│ each EventDto.toDomain() # JSON ─▶ PlayerAccountEvent record
▼
PlayerAccount.reconstitute(playerId, events)
│ new PlayerAccount(...): apply(event) for each, in order
▼
Optional<PlayerAccount> # balance & name folded up from events
The real method is tiny — the elegance is that it delegates:
public Optional<PlayerAccount> find(PlayerId playerId) {
return Optional.ofNullable(eventDtosByPlayer.get(playerId))
.map(events -> mapPlayer(playerId, events));
}
private PlayerAccount mapPlayer(PlayerId playerId, List<EventDto> eventDtos) {
List<PlayerAccountEvent> events = eventDtos.stream()
.map(EventDto::toDomain) // row → event
.toList();
return PlayerAccount.reconstitute(playerId, events); // events → account
}
| # | Step | Detail |
|---|---|---|
| 1 | Look up the event stream | The repo is an in-memory event store: a Map<PlayerId, List<EventDto>> named eventDtosByPlayer. find() asks the map for this player's list of events. |
| 2 | No events → no account | Optional.ofNullable(...) means an unknown player yields Optional.empty(). Nothing is invented — no events, no account. |
| 3 | Deserialize each row | Each EventDto is one stored “row” (playerId, eventId, eventType, json). toDomain() uses the eventType string to pick the record class from a converter map, and Jackson turns the JSON back into a domain event like PlayerRegistered("Judy"). |
| 4 | Reconstitute the aggregate | PlayerAccount.reconstitute(playerId, events) is the @Reconstitute factory. It requires a non-null playerId, then calls the private constructor. |
| 5 | Replay to fold up state | The constructor starts blank (balance = -1, name "DEFAULT NAME") and loops apply(event) over every event in order. Each event nudges state. A left-fold: state = replay of all past events. |
| 6 | Return the live object | The result is the same PlayerAccount you'd have if you'd run those commands live. No new events are enqueued during replay — apply() only changes fields, whereas live commands (deposit(), bet()) call enqueue(). |
Replaying Judy's stream, apply() folds each event into the running state:
| Event replayed | Rule in apply() | Account state after |
|---|---|---|
| — (start) | constructor defaults | name="DEFAULT NAME", balance=−1 |
| PlayerRegistered("Judy") | set name, balance = 0 | name="Judy", balance=0 |
| MoneyDeposited(10) | balance += 10 | name="Judy", balance=10 |
| MoneyBet(4) | balance −= 4 | name="Judy", balance=6 |
| PlayerWonGame(8, …) | balance += 8 | name="Judy", balance=14 |
find(). Change or replay the events and the balance follows for free. That is event sourcing.
find() sits — the save/read mirrorsave() writes events out as rows; find() is its mirror image, reading rows back into events:
save() event ─▶ EventDto.from(id, eventId, event) ─▶ JSON row ─▶ append to map
(event.getClass().getSimpleName() = eventType)
find() JSON row ─▶ EventDto.toDomain() ─▶ event ─▶ apply()-fold ─▶ PlayerAccount
(converters.get(eventType) picks the record class)
save() — writing new events | find() — reading them back |
|---|---|
Commands (register/deposit/bet) enqueue() events; save() serialises each fresh event to an EventDto row and appends it. | Loads the rows, toDomain() each back to an event, then reconstitute() folds them into a rebuilt account. |
| event → EventDto.from() → JSON | JSON → EventDto.toDomain() → event |
Note the type round-trip: save stores eventType = event.getClass().getSimpleName() (e.g. "MoneyDeposited"); find uses that same string to look the class back up in EventDto's converters map before Jackson deserialises.
The classes themselves — read them together, they're short: application/port/PlayerAccountRepository.java (the find/mapPlayer/save trio), application/port/EventDto.java (from/toDomain + the table-schema comment), and domain/PlayerAccount.java (reconstitute + apply). For the pattern behind it, Martin Fowler, Event Sourcing. Background on why we seed test state through the aggregate is in the 17 July learning record.
Want to trace a real save→find round-trip in the debugger, or see how Lesson 0004's payout writes a PlayerWonGame event that this fold later replays? Ask me.