About 6 months ago, I joined the engineering team at Gusto. Gusto makes payroll, benefits, and HR software for small businesses in the US.

After cutting my teeth for a bit on core payroll, recently I’ve been tasked with refactoring the existing code base. Gusto already has some healthy processes,1 but the organization understands the merits of continuously refactoring something that already works.

After wearing many different hats over the last few years, I now find myself wearing just one: helping make the design of existing code better.

The stakes are pretty high: a bug doesn’t just mean a proverbial cat photo fails to get served, but someone might not get paid on time or the IRS might send you a scary letter (a “notice” as they are called). Furthermore, things are guaranteed to change as lawmakers rotate in and out. On top of all of that, every state has a different set of tax laws.2 Payroll software involves time, money, and people: some of the hardest things to deal with in software.

This singular focus of refactoring has been nice. In the ebb and flow of my career, it’s been a little while since I’ve had the chance to breathe and dive deep on certain problems and practices.

Recently, test-driven development and its siblings have been at the forefront of my mind. Previously, I had taken TDD at face value or had misunderstood it: I would write my tests after I had written my code.

While diving deeper into TDD, there was a phrase that stood out to me in this talk by J. B. Rainsberger: Design Pressure.

Design Pressure is the guidance or “pressure” that test-driven development places on your software design.

Design Pressure is the little voice in the back of your head made manifest by a crappy test with too much setup.3 It’s a force that says when your tests become hard to write, you need to refactor your code.

My gut has always known that this was the case, but I could not clearly articulate that TDD is more than just about that tests. I had felt Design Pressure previously, but did not have a name for it and therefore stumbled when trying to express its utility.

This Design Pressure and the code that results is a higher-order benefit of TDD. It’s one that folks often never reach when first starting to write tests. The lower order benefit of writing a test is that you can assert the system works. When first starting out, I assumed that that was the only benefit.

Over the years, that lower order benefit is no longer why I write tests. I write tests for their Design Pressure. Writing tests first produces better code. Writing tests for a class that was written 5 minutes ago is too late. Tests are not just for avoiding clerical mistakes, but also for avoiding design mistakes.

Driving software design with tests has allowed me find much more confidence in the systems we build and refactor at Gusto. Converting unit tests into collaboration and contract tests as I “push” down into the final implementation results in cleaner code with richer tests. The resulting code is correct and robust.

We’ve practiced this with some larger refactors at Gusto and the results have been great: cleaner, faster code with zero regressions.4

Special thanks to Iheanyi Ekechukwu, Phan Le, and Justin Duke for reading early drafts of this post.

  1. We employ code reviews on every pull request, pair on much of the code we write, practice continuous integration, write tests for all code, and more. You know, what you would hope your payroll provider does. 

  2. A funny note about each state having their own tax laws: many states employ their own EIN formats, although the federal government also provides a federally unique EIN. Fellow developers, we should spend more time educating local lawmakers on set theory. 

  3. Gusto is a Rails shop that uses RSpec for writing tests. My rough heuristic for a “crappy test” is one that requires more than 4-5 lets to get off the ground. Once I find myself doing too much setup for the unit under test, it’s time to start breaking things apart. 

  4. Generally, we use the StranglerApplication pattern to swap out the core of an implementation without changing its interface. The new core will be developed with a TDD approach and then connected up to the existing interface.