Live Coding Journal - Apr 7, 2026
Reflections, Learnings, and Mistakes from live coding my JitterTicket Event Sourcing application
Notes from Today’s Live Coding Session
Moving Away from Aggregate-Focused Types
I started the stream innocently enough, renaming EventConsumer to the better-named EventStreamConsumer, since it is responsible for handling a potential Stream of Events, as shown by its interface:
public interface EventStreamConsumer {
void handle(Stream<? extends Event> eventStream);
}
This better separates its responsibility from EventHandler, which is responsible for implementing individual handle() methods for specific Event types.
You may have noticed that I dropped the generic EVENT type, replacing it with ? extends Event).
This is a step in moving away from an Aggregate-centric view of things, where a hierarchy of events relates to specific Aggregates.
For example, where ConcertEvent, associated with the Aggregate Concert, is the parent class of ConcertScheduled, TicketsSold, etc., and CustomerEvent (for Customer) is the parent of CustomerRegistered, TicketsPurchased, etc.
As I continue to move towards an Event-centric view, the generic type gets in the way, since, for example, we may have EventStreamConsumers that want to consume both ConcertScheduled and CustomerRegistered (for whatever reason).
Eventually this will ripple out so that there is only one EventStore instance, instead of having one per Aggregate, e.g., EventStore<Concert,...> and EventStore<Customer,...>.
EventStore Subscription for Desired Events
My main goal this stream was to have all EventStreamConsumer subscriptions to the EventStore specify their desired events to improve performance, as well as making it clear in the code which events it wants:
void subscribe(EventStreamConsumer eventStreamConsumer,
Set<Class<? extends Event>> desiredEvents);
and deprecating the existing subscribe(EventStreamConsumer) method.
An “Olive” Failure Moment (1h01m)
During test-driving this change, Predictive TDD (predicting precisely how a test would fail) once again saved the day.
In my test, I was using my MakeEvents builder to set up the events for the test, and then passed those events to the EventStore’s save(ID aggregateId, Stream<EVENT> uncommittedEvents) method.
However, instead of expecting the test to fail by sending all events to the consumer (as I hadn’t yet implemented the filtering), no events were sent at all.
This was surprising, because I had thought that save() method would send the events along to any consumers.
It turns out, the save(AGGREGATE aggregate) method (that takes an entire Aggregate) is what sends events on to consumers.
The save(ID, Stream<Event>) is more of an internal method, but because it is currently public, I had a misunderstanding of its responsibility.
Once I changed the test to use save(AGGREGATE), the test failed as expected (aka an “Olive”), which allowed me to move forward on implementing the filtering.
TDD Heuristic: Use Test Failures to Examine Your Design
As I said in today’s stream, “TDD is not about writing tests. It’s about exploring the design space.” Tests fail, but it’s when they fail for the wrong reason (or perhaps don’t fail at all), that it’s a good opportunity to step back and examine why that happened.
The New Projection Coordinator (2h28m)
With the Event Store supporting filtering events for consumers, it was time to migrate the ProjectionCoordinator to use filtering for both the catch-up portion (asking the Event Store for all events after a checkpoint event sequence) and for the subscription.
This wasn’t a trivial migration, though, since the NewDomainProjector is now stateful, vs. the stateless/functional implementation in the (old) DomainProjector.
This means that the NewDomainProjector not only holds onto the projection (like a cache), but also needs to track the Checkpoint (latest event sequence processed).
In addition, to prevent race conditions, the projector has to return the projection and the checkpoint as part of the flush() method’s return value.
Next Steps
Next time I’ll finish the NewProjectionCoordinator and NewDomainProjector so that the coordinator lets the projector handle the cache of the projection and the checkpoint.
Join me on my next stream, which I usually do Monday through Thursday, starting at 19:00 UTC on Twitch: https://jitterted.stream.