Nullables & A-Frame Architecture Livestream

In this weekly livestream series, which ran from November 2022 to April 2023, I paired up with Ted M. Young, aka jitterted. We looked at Nullables and A-Frame Architecture as an alternative to Mocks, Spies, and Hexagonal Architecture. Each episode combines a healthy dose of architecture and design discussion with practical, hands-on coding.

The source code is on GitHub.

Episodes

#1: Nullable Die Rolls

Screenshot of “Nullable Die Rolls” episode

In our first session, we introduce the codebase we’re working on: Ted’s yacht-tdd, which is a web-based Yahtzee-like dice game written in Java and using the Spring framework.

We introduce the core concepts of Nullables, and modify Ted’s DieRoller class to be nullable, in place of his existing stub-based approach. Along the way, we have a lot of great conversations about the similarities and differences between Hexagonal Architecture and A-Frame Architecture.

#2: Exploring Architecture

Screenshot of “Exploring Architecture” episode

We use the DiceRoller class to dig into the conceptual differences between Hexagonal Architecture and A-Frame Architecture, and think about at what belongs in the “logic,” “infrastructure,” and “application” layers. Ultimately, we’re able to eliminate the class entirely.

#3: Fetcher Nullability

Screenshot of “Fetcher Nullability” episode

We start our work on the HttpAverageScoreFetcher and HttpScoreCategoryNotifier classes. These adapters are part of Ted’s implementation of Hexagonal Architecture: they retrieve and store data from a separate “Yacht Tracker” service. We spend some time understanding how hexagonal architecture works, then make HttpAverageScoreFetcher nullable. Along the way, we demonstrate the plusses—and minuses!—of using an embedded stub.

#4: State-Based Notifier

Screenshot of “State-Based Notifier” episode

We look at how to make the HttpScoreCategoryNotifier class nullable. This adapter stores data to a separate “Yacht Tracker” service. We demonstrate how to make the adapter expose its state by creating a reusable OutputTracker class, then demonstrate how easy making an adapter Nullable can be.

#5: get() HttpClient

Screenshot of “get() HttpClient” episode

In the last few episodes, we made the adapters for Ted’s scoring service Nullable. These are the “out” or “secondary” adapters in the codebase’s Hexagonal architecture.

Now that the adapters are Nullable, we see an opportunity create a low-level HttpClient adapter that the scoring service adapters can use. This should simplify the Nullability logic in those adapters and open up new opportunities for testing. We start with the get() method and the Nullable’s configurable responses.

#6: Finish HttpClient

Screenshot of “Finish HttpClient” episode

We finish off our Nullable JsonHttpClient adapter by adding support for multiple responses per endpoint, output tracking, and the post() method. Now we’re ready to use it in the rest of our code. In the final few minutes of the episode, we demonstrate how it works by using it in HttpScoreCategoryNotifier.

#7: High-Level Adapters

Screenshot of “High-Level Adapters” episode

We take advantage of our new JsonHttpClient infrastructure to simplify our ScoreCategoryNotifier and AverageScoreFetcher adapters. Now they can be tested easily and no longer need clunky embedded stubs. Along the way, we have lots of conversations about design—test design, input validation, anti-corruption layers, and more.

#8: Nullable Application

Screenshot of “Nullable Application” episode

Now that we’ve finished making our adapters nullable, we’re ready to introduce them to our application code. We start by making GameService nullable, then clean up all the existing tests to use GameService.createNull() rather than creating and injecting dependencies manually. That leaves plenty of time for design discussion, and a start to our next feature: persistence! We design our database schema and decide how to tackle the problem.

#9: Database Dreams

Screenshot of “Database Dreams” episode

Databases! With our initial implementation of nullables complete, it’s time to add persistence. We start by reviewing Ted’s Spring Data JPA spike, then begin creating a GameDatabase infrastructure wrapper from scratch. The goal is to write low-level tests that give us total control over how the wrapper works. Most of our time is spent fighting with Spring Data, but by the end, we’ve got it figured out.

#10: Database Development

Screenshot of “Database Depths” episode

Now that we’ve figured out how to write low-level tests of our database code, we’re ready to crank up the speed and implement our GameDatabase infrastructure wrapper. We finish implementing the saveGame() logic, then move on to the happy-path loadGame() logic. Along the way, we factor out some nice helper methods to make the code clean and easy to test.

#11: Database Depths

Screenshot of “Database Depths” episode

With the basics of our database adapter working, we turn to the real work: error handling. We look at problems such as missing database rows, invalid data types, and incorrect serialization. This prompts lots of conversations about design, including an interesting solution to the tradeoff of unchecked vs. checked exceptions in Java. By the end of the stream, we’ve taken care of all our edge cases.

#12: Database Demarcation

Screenshot of “Database Demarcation” episode

Our database wrapper is pretty much done, but we still have some design improvements to make. We write a test to make sure database updates work (they do), refactor our tests, then look at moving our dice validation logic to the HandOfDice class. That leads to some interesting conversations about how to design our logic code to handle errors cleanly.

#13: Database Denouement

Screenshot of “Database Denouement” episode

It’s a wrap! We finish our work on the database wrapper by focusing on testability. First we add output tracking, so we can easily see what’s written to the database. Next, we create a GameDatabase.createNull() factory and Embedded Stub, so our tests can prevent the wrapper from talking to the database. Part of that work involves configuring responses, including pondering the best way to simulate loading a corrupted game. We finish by looking at what’s coming up next. Can we modify the code so Ted’s GameService class is no longer needed?

#14: Reflective Design

Title screen for “Reflective Design” episode

Our Nullable infrastructure is in place, so we’re ready to take a deeper dive into A-Frame Architecture. We start with Reflective Design: analyzing the existing design to see how it can be improved. We trace through the code from the controller to the domain logic, identifying possible improvements. Then we get ready to refactor by migrating the GameService to use our new GameDatabase abstraction. This turns out to be a tricky problem that’s rife with pitfalls.

#15: Test Design

Title screen for “Test Design” episode

Following our time-honored tradition, we spend as much time talking about design and coding practices as we do making progress on the code. We talk about design and refactoring principles, stengths and weaknesses of object-oriented design, and immutable vs. mutable state.

We make some progress on migrating our GameService to use the GameDatabase abstraction, too. It’s surprisingly difficult: the current tests use a fake to simulate the database, and our new Nullable GameDatabase doesn’t behave the same way. This leads to some discoveries about how to improve the tests’ design.

#16: Eventful Tests

Title screen for “Eventful Tests” episode

What is “good design?” We spend the first part of the episode talking about what makes a design “good,” or not, and the benefits and drawbacks to living on the cutting edge. Then we turn our attention to the controller tests.

At the end of the last episode, we introduced a way to track which games GameDatabase had saved, but it doesn’t clean up the tests as much as we’d like. Eventually, we hit upon a better solution: tracking events. Combined with a somewhat unorthodox approach to sum types in Java, our tests are finally clean and targeted.

#17: Game On

Title screen for “Game On” episode

We’ve had trouble with tests that do a lot of state manipulation. They’re long, complicated, and obscure the intent of the test. Previously, we added events to our GameService class, so we could see how the service was used without having to examine the state of the game. This time, we add the ability to control the state of the service when it’s created. That cleans our tests up nicely, and we finish up the VueController tests before moving on to YachtController.

#18: Dangerous Developers

Title screen for “Dangerous Developers” episode

It’s a rant-heavy episode, with conversations ranging from team structure to dangerous developers, assertion APIs, the “Extract Class” refactoring, and more. Somehow we manage to get a bit of coding in, too. We finish migrating our YachtController tests to use our new fixture- and Nullables-based approach. All that’s left is to turn off the in-memory fake and replace it with our Nullable GameDatabase.

#19: For the Win

Title screen for “For the Win” episode

The final episode of the season! We finish converting the Yacht code from using mocks, stubs, and fakes to using Nullables. This reveals a bug in our database code. We had missed a test of our deserialization logic, but a VueController test triggered the same bug. It was only caught because we used Nullables. Sociable tests for the win!

In addition to finishing up our work on Yacht, we also have our usual conversations about software development and design. This week, we discuss estimation, testing styles, deadlines, and more.

Continue with Season 2

Our work on the Yacht codebase was over, but Ted I had more pairing to do! The series continued with The AI Chronicles, a brand-new codebase solving an interesting new problem.