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

Live Coding Journal - Feb 5, 2026

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


Notes from Today’s Live Coding Session

Pumpkins

Today I introduced the idea of saying the word “pumpkin” during my stream so that when I write these notes, I can easily search through the text transcription of the stream for all instances of where I say “pumpkin”. This way, when I have an aha moment, or mention something I want to note here, including mistakes, learnings, etc., I can easily find them by searching for “pumpkin” as the marker or anchor. I loved this so much, I’m now calling these highlights “pumpkins”. So, enjoy today’s pumpkins!

Testing A Tiny Feature the Wrong Way

At around the 1-hour mark, I was working on adding a small feature to the Concert aggregate, where I could stop ticket sales for the concert by calling a command method stopTicketSales(). However, I temporarily forgot that I was using event-sourcing, so when I wrote the test like this:

@Test
void stopTicketSalesGeneratesTicketSalesStopped() {
    Concert concert = ConcertFactory.createConcert();

    concert.stopTicketSales();

    assertThat(concert.canSellTickets())
            .isFalse();
}

and was about to start writing the code to make it pass, I realized I was testing the non-event-sourced way! (And that’s despite the fact that the test name specifically says the event I expect to be generated!) Sigh. I quickly realized my mistake and changed the assertion to check that the TicketSalesStopped was generated:

@Test
void stopTicketSalesGeneratesTicketSalesStopped() {
    Concert concert = ConcertFactory.createConcert();

    concert.stopTicketSales();

    assertThat(concert.uncommittedEvents())
            .containsExactly(
                    new TicketSalesStopped(concert.getId(), null));
}

I predicted that the test would fail because I didn’t write the code to generate the event yet, but it failed for the wrong reason! Once again, “calling my shot” (predicting precisely how the test would fail) saved me from a bad test.

TDD Prediction Principle:

It’s not good enough to just have a failing test, it has to fail for precisely the right reason, i.e., because the code doesn’t implement the behavior that you want to add, not because of some other problem (such as the wrong test setup or a misunderstanding of the codebase’s current behavior).

The problem here is that the ConcertFactory.createConcert() helper method creates an unsaved Concert object, which means that when I check the uncommittedEvents(), it isn’t empty, but instead has the ConcertScheduled event. Oops. Changing the test setup to create a “reconstituted” Concert object fixed the problem:

@Test
void stopTicketSalesGeneratesTicketSalesStopped() {
    Concert concert = Concert.reconstitute(
            List.of(
                    ConcertFactory.Events.scheduledConcert()
            ));

    concert.stopTicketSales();

    assertThat(concert.uncommittedEvents())
            .containsExactly(
                    new TicketSalesStopped(concert.getId(), null));
}

Lesson Learned: The test setup and assertions for the events is still a bit clunky, despite the factory methods and builders that I have. At some point I need to move to a higher level of abstraction for the setup and assertions. One way to go is to use strings to describe the events, but that loses compile-time safety, and more importantly, doesn’t let me auto-complete. Likely the next thing I try is some nested builders, but I need to do more experimentation to find the right structure that helps with autocomplete, but is also readable.

Red Test Does Not Mean New Test

Something I often talk about in my TDD courses (as well as on my stream) is that the “red” (failing test) in “Red-Green-Refactor” (the high-level TDD cycle) doesn’t require a new test. In fact, probably 20–30% of the time, my first failing test is a modification of an existing test that is currently passing.

I think most often this happens when I’m adding functionality to a class, like when I added the ability to stop ticket sales to the Concert aggregate. Before I could implement the functionality of stopping the ticket sales, I had to make sure that the behavior for a new Concert was that ticket sales were allowed. I didn’t need a new test, I already had the concertScheduledUpdatesConcertDetails() test, so I just needed to add an assertion to check that the canSellTickets() method returned true. (This was technically the “zero” case in the Zero/One/Many guideline, aka ZOMBIES.)

Feature Envy Trap

After I added the stopTicketSales() method, and the internal canSellTickets boolean, I realized that I had a code smell heuristic that I hadn’t articulated before (around the 1h33m point):

Precursor to Feature Envy Heuristic

If an entity has state that it’s not using internally to make decisions, then why is it holding onto that state? If it’s only exposing it and not using it, that’s just ripe for future feature envy, where a client of the entity uses that information to make a decision.

Resolution: either the entity needs to use that state internally, or it should be removed (or moved) to an entity that needs it to make a decision.

Once I noticed this (awareness of these things is 90% of the battle!), I realized I was missing a necessary business rule where if tickets sales were stopped, we should not allow the sellTickets() command method to succeed. (I decided not to implement that yet, since I wanted to get to the core of the Concert Sales Processor.)

Who’s Job Is It? Processor vs. Aggregate

My main goal for today’s stream was to build my first Event Sourced Processor and had chosen the idea of a separate “Processor” whose job was to track ticket sales. When a concert sells its last ticket, the processor would invoke the stopTicketSales() command on the Concert aggregate. After some back-and-forth thinking out loud and discussion with the stream viewers (thanks chillMute!), I realized that I was about to design a Processor that needed information that the Concert aggregate already had! Concert already knows about ticket sales and precisely when the last ticket is sold (via the sellTickets() command method).

I admit to being a bit disappointed that there was no need for the “Concert Sold Out Processor”, but I’m still learning about this aspect of Event Sourcing, and the discussion of “who’s responsible” was really useful. Luckily, I already had other Processors that I wanted to implement.

We dug into the details of the “Concert Started Processor”, which would be responsible for tracking when a concert starts (based on its Show Date/Time), and invoke stopTicketSales() on Concert when that time comes around. Figuring out how to implement this took us down a few dead-ends (e.g., Spring’s scheduling support was not the right mechanism), and ended up deciding to use Java’s built-in ScheduledExecutorService to schedule the stopping of ticket sales along with a Map<ConcertId, ScheduledFuture> to keep track of the concerts (so we can reschedule the futures if the concert itself gets rescheduled). It’s annoying that ScheduledExecutorService doesn’t have an easy way to specify a specific date/time to execute the task (it requires a duration from “now”), but it’s just a bit of simple date math to figure this out.

Next Steps

We ended with a failing test for the new ConcertStartedProcessor, and a design plan for how it’ll probably be implemented, along with a few tests (a Test List!) that we’ll start with.

I hope you’ll join me on my next stream, usually starting at 20:00 UTC on Monday thru Thursday 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.