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

Live Coding Journal - Feb 19, 2026

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


Notes from Today’s Live Coding Session

Spring Boot 4.0.3 Upgrade

Before doing any more functional work, I wanted to (finally!) upgrade from Spring Boot 3.5.9 to 4.0.3.

I wanted to try out using the Arconia CLI to drive the OpenRewrite migration scripts. I ran it, but nothing seemed to happen. Apparently I missed the fact that you needed to type arconia help to get the list of commands and options. I think this is a poor starting user experience as -h, --help, typical options for CLI tools, produced stack traces, where I expected to see some sort of help. Most CLI tools will at least display something when you run them, so it’s strange that Arconia doesn’t display anything at all.

Following the migration guide, before doing the actual upgrade, I did a minor upgrade to 3.5.11, the latest in the 3.5.x series, and made sure all the tests passed (they did). What was confusing for a bit was that I had an override property to ensure that I was using JUnit 5.14, as I was using the recent Parameterized Classes feature. I removed the override thinking that Spring Boot 3.5.11 had a recent enough version of JUnit, but it only had 5.12.2, which wasn’t recent enough.

Once I got that cleared up, I ran the Arconia migration scripts and, unsurprisingly, code relating to how I configure the Jackson (JSON) ObjectMapper no longer worked. I say unsurprising because Spring Boot 4.0.x uses Jackson 3 instead of Jackson 2, where enough has changed that the migration scripts currently don’t handle all the necessary changes. The biggest annoyance was figuring out how to configure INCLUDE_SOURCE_IN_LOCATION, which was way more difficult to solve than it should have been. I’ll be writing up a deeper post on this, with perhaps some ideas for additional OpenRewrite recipes to cover the situation.

Once I got past the compilation issues, I ran all the tests and noticed a failure in the CSV tests that were sensitive to changes in JSON serialization. Apparently, the default way that dates and times are serialized in Jackson 3 is ISO-8601, which is a much better format, but different from the previous default. I was glad that tests caught this, but dismayed that it was only caught as a part of a CSV test instead of a dedicated serialization test. I wrote the missing serialization test as a characterization test, and then fixed the failing CSV tests to use the new date/time format.

After finishing the Jackson migration, all the Spring-related tests failed with a weird NoSuchMethodException for computeIfAbsent in the SpringExtension. I quickly realized that I had forgotten to take out the JUnit version property, which was now forcing an old version (5.14.0) instead of letting Spring Boot pull in the 6.0.3 that it needed.

Projection Misconfiguration

Once I got the application running again, I realized there was a problem with the concerts shown on the “On-Sale Events” page (around 1h12m into the stream). The problem was that one of the concerts shown should not have been on sale because its show date/time was in the past. I realized that I had miswired the projection when plugging in the “All Concerts” projection in a previous stream (to fix the event viewer). Because both the “All Concerts” and “Available Concerts” projections use the same types for the projection contents (I took a shortcut by reusing AvailableConcerts for both), I didn’t realize I was injecting the wrong projection into the on-sale page. It wasn’t a difficult fix, but in a production system, I would have been more disciplined about making sure that the two projections used completely separate types.

Extending CommandExecutorFactory for Creation

I thought adding the “creation” command type to the CommandExecutorFactory was going to be complex, but it turned out to be straightforward once I realized that calling a static method (to do the creation) was no different than calling an instance method. Once again, getting my head wrapped around the generics needed for the wrap and the command execution took a bit of time, but was happy with the result:

public CreateWithParams<ConcertId, ScheduleParams> wrapForCreation(
        CreateWithParams<Concert, ScheduleParams> command) {
    return scheduleParams -> {
        Concert concert = command.execute(scheduleParams);
        concertEventStore.save(concert);
        return concert.getId();
    };
}

with the concrete “schedule” command defined like this:

public CreateWithParams<ConcertId, ScheduleParams> createScheduleCommand() {
    CreateWithParams<ConcertId, ScheduleParams> command =
            commandExecutorFactory.wrapForCreation(
                    scheduleParams -> Concert.schedule(
                            ConcertId.createRandom(),
                            scheduleParams.artist(),
                            scheduleParams.ticketPrice(),
                            scheduleParams.showDateTime(),
                            scheduleParams.doorsTime(),
                            scheduleParams.capacity(),
                            scheduleParams.maxTicketsPerPurchase()));
    return command;
}

The expectation is that any creation command is going to return the ID of the created aggregate, since the client doesn’t supply the ID but would need to know it. For example, we probably want to display the created aggregate to confirm that the creation worked, so we need the ID to use as part of an HTTP redirection. I ended up not bothering with that, since this is an example app, and so simply redirected back to the on-sale concerts page.

The Test Fixture is Inside the Controller!

With the “inside” of the schedule functionality complete, I test-drove the UI to wire it up. On the way, instead of putting the test fixture for the ScheduleConcertController in the test, @Suigi suggested going the Nullables route. At first, I didn’t think it’d work, because the test needed access to the EventStore Test Double as well as the controller, and Java doesn’t have a way to return multiple values. The way I typically do this in the test context is to use Extract Method, and then IntelliJ IDEA creates a record containing the objects the test needs as the return value. However, there’s no reason not to do this in the production controller class! Test code in a production class? Blasphemy, I know.

Here’s what it looks like:

static Fixture createForTest() {
    var concertEventStore = InMemoryEventStore.forConcerts();
    ScheduleConcertController scheduleConcertController =
            new ScheduleConcertController(new Commands(CommandExecutorFactory.create(concertEventStore)).createScheduleCommand());
    return new Fixture(concertEventStore, scheduleConcertController);
}

record Fixture(EventStore<ConcertId, ConcertEvent, Concert> concertEventStore,
               ScheduleConcertController controller) {}

So now the test can call createForTest() and access the in-memory EventStore as well as the controller:

@Test
void postScheduleNewConcertCreatesConcert() {
    var fixture = ScheduleConcertController.createForTest();

    String redirect = fixture.controller().scheduleNewConcert(
            new ScheduleConcertController.ScheduleForm("Daylight Noise", 35, "2026-03-14", "19:00", "18:00", 75, 2));

    assertThat(redirect)
            .isEqualTo("redirect:/concerts");
    assertThat(fixture.concertEventStore().allEventsAfter(Checkpoint.INITIAL))
            .hasExactlyElementsOfTypes(ConcertScheduled.class);
}

And if a test doesn’t need to see the EventStore, it can just ignore it.

Projection vs. Read-Model vs. Query and Validation

At around 3h35 in the stream, I discussed the similarities and differences in the terms “Projection”, “Read Model”, and “Query”, especially as it relates to Event Modeling. It’s a bit confusing, because I see “Query” as a major component and none of the Event Models I see incorporate it. This was all part of figuring out the next feature, which is validating the Schedule Concert to ensure that we’re not double-booking artists for the same date.

Next Steps

Next time we’ll dig into implementing the validation, as I think it’s important functionality that I haven’t tackled yet in this event-sourcing example application.

Join me on my next stream, which I usually do Monday through Thursday, starting at 20: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.