Instant Project Dashboards for AequilibraE
Over the last two months, I have been interning with Outer Loop on the integration of web-based visualization …
Photo by Clean Code Guy
When I started writing software professionally, one of the most compelling practices I encountered was pair programming combined with test-driven development (TDD). I no longer follow it as strictly as I once did, but its central idea has stayed with me and continues to shape how I approach development.
For those unfamiliar with TDD, the practice begins by writing a test that you know will fail because the implementation does not exist yet. You then write just enough code to make it pass, refactor as needed, and repeat.
What I like most is that you can write each test as though the software already works exactly the way you want it to. In effect, you ask: What should this look like when the work is done? The tests define the shape of what you are building, and the growing test suite becomes a shared source of truth—not only for whether the software works, but for what it is meant to do.
When combined with pair programming, one person can focus on defining the behavior through tests while the other focuses on implementing it. For the usual reasons—deadlines, limited resources, and competing priorities—I have not had many opportunities to work this way with a dedicated partner in recent years.
Recently, though, I realised that I lived in an age where my pair partner was always available to collaborate with.
Pair programming with an AI is genuinely powerful, but not for the reason people usually reach for (“it writes code fast”). It’s powerful because it collapses the loop between describing what you want and seeing it exist. I often discuss the concept of shortening feedback loops in development; minimising the amount of time between attempting something and finding out if that attempt was successful.
The trap, though, is the “vibe coding” failure mode: you describe something fuzzy, the AI produces something plausible, and you end up reviewing a large blob of code whose shape you never actually decided. You’re no longer the architect — you’re a reviewer of someone else’s guesses.
Driving the AI with a TDD didn’t feel like that. It gave me a way to define the exact contract the agent had to satisfy, before it could go off the rails and over engineer a solution.
The pattern I settled into was a red/green/refactor straight out of the Agile playbook. I write the tests, the AI is responsible for making them green.
I’d hand over a failing test that described the next slice of behaviour, with an instruction like:
I’ve made changes to tests that describe my desired behaviour. We are doing red/green/refactor TDD. You are in charge of making tests green. After they are green, commit and return control to me.
Then I’d just… keep extending the test. Each round, the prompt was often as short as:
go again
That’s it. The test is the specification. The AI reads the diff, implements the missing behaviour, runs the suite, lints, type-checks, commits, and hands control back.
The project I was working on is called popdelta — a small library for treating population evolution (ageing, births,
deaths, migration) like a git repository. Each step in the evolution is just a commit against a set of tables.
I had decided I wanted an on-disk store with a current_state/ folder and ordered deltas/.
I didn’t describe the API in prose. I described it as an assertion:
store = PopulationStore.create(population_dir, make_population())
assert (population_dir / "current_state").exists()
assert (population_dir / "deltas").exists()
assert (population_dir / "deltas" / "1_initial_commit").exists()
Green. Commit. go again.
Next I pushed on the editing workflow — and crucially, I used the test to pin down semantics I cared about as an architect, like immutability of the input and the fact that a commit returns the new state:
store.add_to_current_delta(
table="persons",
rows={"add": pd.DataFrame({"person_id": [6], ...})},
)
# Before committing, the original population should remain unchanged.
assert population.persons.shape[0] == 5
new_pop = store.commit_current_delta()
assert new_pop.persons.shape[0] == 6
assert Population.from_dir(population_dir / "current_state") == new_pop
Green. Commit. go again.
Then the subtle one — operation ordering. If you delete a row and replace a column in the wrong order, the column has the wrong number of values. I encoded that as the contract itself:
store.create_delta("conflicting rows cols")
with pytest.raises(ValueError):
# Ordering matters and we can't establish it with keyword args:
# if the row delete happens first, we can't supply 6 age values
# when only 5 rows remain.
store.add_to_current_delta(
table="persons",
columns={"replace": current_age + 3},
rows={"delete": pd.DataFrame({"person_id": [6]})},
)
# Sequenced correctly, it works:
store.add_to_current_delta(table="persons", columns={"replace": current_age + 3})
store.add_to_current_delta(table="persons", rows={"delete": pd.DataFrame({"person_id": [6]})})
I never explained how to enforce that. I asserted the behaviour I wanted and let the AI find the implementation that satisfied it.
Decades of experience don’t make me faster at remembering whether it’s DataFrame.merge(..., indicator=True) or the exact pandas incantation to drop a
column. That knowledge is real but low-value, and fighting syntax is a tax on my actual job.
What is high-value is deciding the shape of the thing:
Writing tests first allowed me to make those decisions, and only those decisions. Then those design decisions
become part of the implementation contract that the agent has to execute. The agent can then handle the pandas, the Parquet I/O, the manifest
serialisation — and when ruff complains that a function is too complex, it refactors without me
asking.
I get more control over the architecture because I’ve stopped typing the implementation.
By the end, the loop looked like this:
go again.It’s the pair-programming-plus-TDD experience that captivated me early in my career — except my pair never tires, never skips the test run, and never argues about where to put the braces (a hill I am always willing to die on). My entire job becomes authoring the contract and judging whether it’s the right one.
That’s a very good place for a software architect to spend their time.
Over the last two months, I have been interning with Outer Loop on the integration of web-based visualization …
Over the past 8 weeks we – Evelyn Bond and Tyler Pearn – have been working with Outer Loop as part of the University of …