Have fun learning about Test-Driven Development with JitterTed's TDD Game

Live Coding Journal - Mar 30, 2026

Reflections, Learnings, and Mistakes from live coding my JitterTicket Event Sourcing application


Notes from Today’s Live Coding Session

Clarifying Event in Event-Sourcing

Once again I caught myself not being clear (to myself!) about what an event is in event-sourcing. Not only is it something that has happened (a “fact”), but it must be related to some “thing”. Whether that thing is an Aggregate (Concert, Customer), or an Entity (Ticket?), or some other concept, in order for it to be a fact, it must be related to something identifiable. This is why an event like “CalendarDayEnded” makes no sense for event-sourcing: which thing are we talking about? What thing’s state has changed as a result? (If no state has changed, then it can’t be an event in event-sourcing!)

It’s fine for “CalendarDayEnded” to be a Collaboration Event, i.e., for communicating information across processes. In that case it might trigger a “Translator” to:

  • Do a query for all Concerts that have a show date of the day that ended, and
  • For each Concert (only one for our single-venue system), issue a Command to the Concert to “close”.

Then we’d end up with a “ConcertClosed” event (with the ID for that particular Concert), but only because of the Command that was issued by the Translator.

“Event Must Have ID” Heuristic

Any event that represents a change in state, i.e., an event in an event-sourcing system, must have some identifier that associates the change in state with the “entity” or value that changed (the identifiable state). Events without an identifier can exist, but they must be some other type of event, e.g., Collaboration or Integration event that communicates information across process boundaries.

Separating “Tickets On Sale” from “Concert Scheduled”

In the current implementation, tickets become available for purchase as soon as the concert is scheduled. In the real world, this is not always (or even often?) the case. This will give me an opportunity to explore event (or “schema”) evolution when adding a new feature, especially when it’s modifying the contents of an event. In this case, we’ll need to add the “on-sale” date and time to the ConcertScheduled event. This is the somewhat easier case, since we’re only adding new properties to an existing event (vs. changing or removing properties, which is harder). As is typical for adding new properties, we need good defaults to “fill in” values for events created prior to this change.

Improving Catch-Up Processing via Filtering Event Types

Since “on sale” is very similar to “stop sales”, in that both require a future alarm to be set for when that date/time come around, I looked at the ConcertStartedProcessor to see what would need to change. As I examined how the alarms get set, I realized that since the alarms are not persisted, the Processor needs to catch up from the beginning of the entire event stream every time. Since it only cares about scheduling and rescheduling concerts and stopping ticket sales, that would be fast, except for the fact that the EventStore doesn’t support asking for events of specific types, only the parent class that relates to each aggregate, i.e., Customer and Concert. This means in addition to the ~hundreds of ConcertScheduled events that we care about, we’d get thousands (or hundreds of thousands) of TicketsSold events that would be ignored. In one of my sets of sample data, where there were ~5MM events, this took over 2 minutes to load! (Yes, there are probably some improvements to make that faster.) However, we end up ignoring almost all of them, making it extremely wasteful.

When I hacked in this change, it took 4 seconds to load and process 101,000 events for the RegisteredCustomers Projector, significantly faster and much less wasteful than loading 5MM.

Redesign of EventConsumer Interface

I spent around 90 minutes (around 17m to 1h47m in the video) adding the ability to specify specific events to load through the event store’s allEventsAfter() method. Once I proved that it would drastically increase performance (that 4 minutes is now around 2 seconds), I looked at how that would fit with the design of the EventConsumer interface, which both Projectors and Processors implement. The main question is: how should an event consumer signal that it’s only interested in specific events? I think there are only two options:

  1. When the consumer registers/subscribes with whatever publishes the events (EventStore in this case), it sends a list of events along with the subscription.
  2. The EventConsumer interface defines a List<Event> interestedEvents() method that gets called from the publisher when the consumer is registered.

The problem with option 1 is that this happens during startup and separates where the consumer is registered with the EventStore (in configuration) from where they’re processed (in the consumer). While I’m not fond of callbacks like this, I’m less fond of having this knowledge of the events separated.

ProjectionCoordinator Disappears?

Looking at redesigning EventConsumer led to an exploration of the ProjectionCoordinator design. The coordinator is a persistence “wrapper” and cache around the actual projection code (a DomainProjector) that does the actual processing of events (and has no state itself). It works nicely, because the DomainProjector doesn’t need to know about peristence (or caching of the projection), or even that persistence is happening, other than returning changes since the previous time it ran (the Delta), which makes the persistence easier. However, since a Projection is a Cache of computed values, maybe the cached projection should be inside the Projector? In other words, turn it inside out. This would mean the ProjectionPersistencePort would be injected into the Projector, and the ProjectionCoordinator would go away.

Next Steps

Since it’s unclear which design (wrapper vs. injecting persister into the projector) is better, next time I’ll try out the injection into a new PersistedProjector for the Registered Customers and Scheduled Concerts projectors and see which one looks better.

Join me on my next stream, which I usually do Monday through Thursday, starting at 19:00 UTC on Twitch: https://jitterted.stream.


Make Your Code More Testable™️

Don't miss out on ways to make your code more testable. Hear about new Refactoring techniques, improving the Test-Driven Development process, Hexagonal Architecture, and much more.

    We respect your privacy. Unsubscribe at any time.