Live Coding Journal - Apr 6, 2026
Reflections, Learnings, and Mistakes from live coding my JitterTicket Event Sourcing application
Notes from Today’s Live Coding Session
Splitting EventConsumer
It turned out that in trying to make the single EventConsumer interface apply to both Projectors and the ProjectionCoordinator didn’t quite fit.
Pure Domain Projectors need to advertise the events they want (“desired events”), and do so by implementing one or more handle() methods with a specific event type.
However, the Projector needs the persistence and catch-up support provided by the ProjectionCoordinator.
I couldn’t figure out a way to make it work without doing delegation of the handle() methods, which makes no sense.
Solved this by splitting it into an EventConsumer (perhaps a better name is EventStreamConsumer), which implements a handle(Stream<? extends Event> events) method and EventHandler, which is where the specific handle() methods would be implemented, and provides a Set<Class<? extends Event>> handledEventTypes() that uses reflection to collect the desired events.
Now, ProjectionCoordinator implements EventConsumer and is in charge of (among other things), catching up with the event store and subscribing to new events.
Once EventStore supports desired events for subscribe, ProjectionCoordinator could do this on startup:
var desiredEvents = domainProjector.handledEventTypes();
// the following now works across all event store implementations
Stream<EVENT> eventStream = eventStore.allEventsAfter(cachedCheckpoint, desiredEvents);
updateProjection(eventStream);
// we'll do this next time
eventStore.subscribe(this, desiredEvents);
New Domain Projector Design
Since the Projector implements individual event handle() methods instead of routing all events through a single method, it is no longer a stateless/functional implementation.
It has to retain state across (potentially) multiple invocations of the various handle() methods, but that’s fine.
Squinting a bit, this change makes a Projector look a lot like the current Aggregate implementations, where the initial state is loaded from the event store, events are applied to evolve the state, and we can ask for new “uncommitted” events that are generated.
And now a Projector gets its initial state loaded from persistence, events are applied via the “handle” methods, and we want to ask it for changes to its state.
This is good, as Aggregates have always been a projection of events, centered around the properties of the Aggregate.
Once I realized that, I wanted to change the Domain Projector’s method that returns the delta (changes) to uncommittedChanges(), but unlike Aggregates which get thrown away once they’ve done their job, Projectors cache their Projections in memory.
This meant I needed to “reset” the changes, so I initially thought I’d need a separate flush() method that would clear it, and added it (but didn’t implement it!) while test-driving (2h17m46s in the video).
However, this could lead to issues where the changes were retrieved, but the flush was never called, potentially corrupting the persisted projection.
@Suigi suggested returning the changes as part of the flush() and get rid of uncommittedChanges() and that’s what I did instead.
Even though this violates Command-Query Separation (CQS), it turned out to be a better match for supporting persistence.
And I’d argue that this is one of the rare exceptions to CQS, much like a Stack’s pop() both modifies the stack and returns a value.
Moving Away from Aggregates – Someday
I’ve been dropping the generic EVENT during these changes, since the original intention of that generic type was to capture the base class for events related to a specific Aggregate, e.g., ConcertEvent that is the parent of all Concert-specific events.
As I move away from this Aggregate-centric way of looking at things, the generic type gets in the way and, instead, we use the event filtering (“desired events”) to capture the events that Projectors, Processors, and (eventually) Aggregates are interested in.
Next Steps
Finish the conversion of the ProjectionCoordinator to use the NewDomainProjector, and update the Event Store’s subscribe() method to accept desired events.
Then I’ll convert the AvailableConcertsProjector to this new model and see how it fits.
Join me on my next stream, which I usually do Monday through Thursday, starting at 19:00 UTC on Twitch: https://jitterted.stream.