IEEE Software - The Pragmatic Designer: Build Stable Triangles

This column was published in IEEE Software, The Pragmatic Designer column, September-October 2025, Vol 42, number 5.

ABSTRACT: By adding a short, clear specification to an implementation and tests, developers build a stable unit: a stable triangle. Each leg can be analyzed, letting developers catch bugs and improve quality. They are a partial antidote to rising complexity and encourage edits in suitable places.


Pre-publication draft. Please click this official link so your view counts in the IEEE's records of article views – plus the IEEE site has profesionally typeset PDFs.

It’s time for developers to deliver units of code consisting of a specification, an implementation, and tests. This simple idea deserves a simple name. I call it a stable triangle because it reminds me of how mechanical engineering structures get their stability from triangles.

Some readers may already have a bias against specifications, so take a look at the stable triangle shown as pseudocode in Figure 1. Notice how normal it looks. Anyone who was worried that a specification (a spec) is a wall of text should be exhaling in relief. In fact, the spec is the shortest part.

Specification Implementation Tests
Returns a list
containing
exactly one of
each distinct
element in inputs.
removeDups(List inputs) --> List
  var map = new Hashmap
  foreach x in inputs
    [ map.put(x, 1) ]
  return sort(map.keys)
isEmpty(removeDups(‘’))
isExactly(removeDups(‘a’), ‘a’)
isExactly(removeDups(‘a’), ‘a’)
containsAnyOrder(
  removeDups(‘ab’), ‘ab’)
containsAnyOrder(
  removeDups(‘aba’), ‘ab’)

Figure 1: A stable triangle for removeDups consisting of a specification, implementation, and several tests.

Specs are an old idea, just like tests. Today, developers typically deliver an implementation and tests, but it wasn’t always this way. For decades, developers delivered only the implementation code. Starting around the year 2000, practices shifted and developers began including tests alongside the implementation.

Consider what else was changing. In the 1990s, software was typically released just a few times per year, run in isolation on personal computers, distributed on compact discs, bought in brick-and-mortar stores, and sold in shrink-wrapped boxes. As the world grew more complex and less forgiving, developers improved their practices. Tests improved code reliability and developer productivity, so they were a good antidote for rising complexity.

Today, software is woven into our lives to the point where a bug can lock you out of your car, a missed email can trigger a tax audit, and a failed software update can lock millions of people out of their computers. Most people interact with software from the moment they wake up (perhaps from an alarm on their phone) until they go to bed (perhaps with software running their toothbrush). Software bugs pervade our lives, so our software engineering techniques aren’t working well enough.

A quarter century ago, developers faced with rising complexity dug into the software engineering toolbox and pulled out testing. The world has grown more complicated and it’s time to pull out another complexity-reducing technique: specifications. In the next few sections, I’ll explain what happens when you embrace specs, how they enable you to catch more problems, and how they discourage edits in inappropriate places.

Developers are understandably allergic to specifications. They associate them with whole-program specifications, specifications “thrown over the transom”, waterfall processes, and non-experts oversimplifying the job of software development. Let’s examine specs from a different perspective.

Imagine that you own a factory that makes various bolts. Your customers are price-sensitive. To help them choose the right bolt, you offer specifications, telling them that your expensive stainless steel bolts are suitable for wet conditions and the cheaper ones are not.

Changing the perspective makes a world of difference. Developers aren’t allergic to all specifications; they are allergic to imposed specifications. The factory isn’t imposing specifications, it’s offering specifications. When you offer specifications, you are explaining your creation. Developers should offer specifications for their code so that clients know how to use it. When you design and implement code, you are the world expert on it: nobody knows that code better than you. When you offer your clients specs, they know what it does and you can warn them about any tricky bits.

Anatomy of a stable triangle

A stable triangle has three corners: specs, implementation, and tests. The spec says what the implementation promises to do, the implementation has the mechanism to do it, and the tests check that the implementation does what the specs promise. You may know that already.

What’s less commonly known is that having all three parts provides stability. Let’s approach that idea gradually and first consider what happens when we skip tests and specs, delivering only the implementation. What can and can’t we do with just an implementation? Without tests, we forfeit the assurance that edits don’t break what already works. Less obviously, we have no record of what the system is supposed to do – except in our memories – because tests provide a durable record of intended behavior. Hopefully you are already sold on tests, so let’s move on.

What if we had tests, but no specs? To some extent, tests act as specs, but unit tests check specific cases, not the general case. Unit tests can assert that add(1,1) is 2 and add(1,2) is 3, and so on, but those are specific cases. Tests are a safety net, but a net with many holes. Without a spec, there’s no durable record of what the system should do in general. There are two big implications: bugs and implementation details.

Without a spec for the general case, the idea of a “bug” is subjective. Imagine that your team’s code passes its tests but a user says there is a bug, pointing to an untested case. How do you decide if it’s a bug? Since there’s no test for that case, there’s no test to act as a spec either. One developer might agree with the user but another might disagree, insisting it’s not a bug. When we have a general spec, it’s easy to decide what’s a bug: A bug is a behavior of the implementation that breaks a promise in the spec.

Most developers believe they can identify the “implementation details” and can avoid testing them. But what is an implementation detail, exactly? It’s any behavior of the implementation that goes beyond the spec. For example, the code in Figure 1 sorts the list before returning it. Without a spec, developers may disagree about whether sorting should be tested. With a spec, it’s easy to decide: test everything that’s promised in the spec, ignoring anything else the implementation does (i.e., sorting and using a hashmap).

Stable triangles let us compare each part with the others, which gives us confidence that the entire thing does what we expect.

A stable triangle

Figure 2. A stable triangle has a specification, implementation, and several tests (the corners). Each leg (A, B, and C) can be analyzed, which provides stability.

Why now?

Developers should embrace specs because the world has changed in gradual and abrupt ways. The gradual changes are the slow rise in the complexity of our systems and the quickening of the development cycle. The abrupt change is generative artificial intelligence (GenAI) that can write and edit code.

Developers embraced testing when compute power became cheap. They were confronting rising complexity and they used the compute power to run tests. There’s still more opportunity to use stronger testing, such as model-based testing, property-based testing, and fuzzing.

That cheap compute power also sped up development cycles and helped developers with complexity. Today, some developers are lucky and experience nearly instantaneous development cycles (i.e., code that’s continually incrementally compiled and deployment happens in seconds). Most are not so lucky and must wait hours or days.

That brings us to the elephant that has trumpeted into the room: GenAI. We all expect a big shakeup but are unsure about when and what. Here, I’ll make some observations about the nature of software design and engineering that I hope are fundamental and stay relevant despite our uncertain future.

People have cognitive limits. As they solve problems, they can keep only so many ideas in their head. They have an easier time solving problems that are well-defined and can be reasoned about locally. GenAI seems to follow this pattern too. It has limits on its attention and the amount of context it can use productively. It works better on well-specified problems because it can check that its solutions are correct. And it solves big problems by breaking them into smaller problems, so local reasoning is desirable.

Specifications seem to help people and GenAI in similar ways. Specified problems are well-defined problems. When a written spec has ambiguities or oversights, as they often do, it’s possible to identify them and improve the spec. As the saying goes: a problem well-stated is half-solved.

A spec makes it possible to create different solutions. Without a written spec, both humans and GenAI must guess (as discussed above) about implementation details and bugs. From a spec, you can write a suitable set of tests, and with both specs and tests in hand you can write one or more implementations.

Nesting specs makes it possible to limit what’s on your mind. Both people and GenAI solve problems through a divide-and-conquer strategy. If you have a spec for the overall problem and specs for each of the sub-problems, you can collapse entire sub-trees of the problem and think only about their specs. For example, your car audio system might need 12 volts and there is a lot of complex engineering to ensure a stable 12 volts, but you can collapse all of that complexity into its spec – there’s 12 volts on this wire – freeing those details from your mind and making space to work on the problem at hand. This is exactly Edsger Dijkstra’s argument for separation of concerns: when we focus on one aspect of a problem, it’s not that we are blind to complexity elsewhere, it’s that we allow our full brainpower to work on one aspect of the problem before turning to the next.

Evolution

As developers, we’d like to believe that more time leads to better code. Instead, what happens in practice is that systems grow buggier and more complicated with each edit. Why is this? Some edits degrade the code. They change code that was working, complicating it, and sometimes even introduce new bugs.

Consider two scenarios where you want to add a feature. First, imagine that you have an unspecified method with a vague purpose, something like handleEvents. That vague method looks like fair game to be edited. So tempting! Just add a few lines of code, write one more test, and you’re done. Second, imagine that you find a stable triangle with a well-defined spec, something like hideElements. As soon as you see that clear spec you pause. This is already a cohesive unit. Your edit doesn’t fit – and you might break callers depending on the existing contract.

Stable triangles encourage edits in suitable places. They invite you to compose triangles rather than edit an existing triangle. This is where evolution connects to process. A developer who scatters edits across the code isn’t building up coherent, stable, trustworthy units. A developer who focuses on one thing, gets it working, checks its quality, then moves onto the next is building up value.

Confidence from simplicity

When I write specs, I strive to keep them at 1-2 lines of text. For simple methods I just say what happens in the success and failure cases, often using this template: “Returns X if Y, otherwise Z.” When the method is more complicated, I ask myself if I can simplify it. Sometimes I can. Sometimes it’s hard to describe because my vocabulary (i.e., the types in my program) is too simple, and adding a type lets me state the spec simply. The more complex my spec is, the more testing I must do and the less I trust it will work as I expect, so it’s worth my time to simplify.

Stable triangles are an affordable luxury. Tests and implementations become stable and easy to analyze when you add specs. Working without specs forces me to guess how the code works, so it’s a self-imposed cognitive impairment. I’m long past any “real programmer” bravado where I pride myself on writing tricky code. It’s hard to write good code and I’m more likely to succeed when I can navigate the options to find simple code. By cycling through the spec, implementation, and tests, I engage in a virtuous cycle where I evaluate the problem from those distinct perspectives and look for ways to simplify. [1]

Most of all, I appreciate the incremental gratification. Some developers zoom across the code making edits in many places, like a modern Icarus. I feel a bit like Daedalus, focusing on one thing at a time, accumulating each stable triangle as a small win, knowing those wins add up, and I have the confidence that each stable triangle will support what comes next. The higher you fly, the more you must trust your contraptions.

Pre-publication draft. Please click this official link so your view counts in the IEEE's records of article views – plus the IEEE site has profesionally typeset PDFs.

References

  1. G. Fairbanks, “Fix Tech Debt With Virtuous Cycles” in IEEE Software, vol. 40, no. 02, pp. 111-116, March-April 2023, doi: 10.1109/MS.2022.3228623.