This is a card in Dave's Virtual Box of Cards.

Tiger Style!

Created: 2023-05-08
Updated: 2024-04-15

I recently watched Joran Greef’s talk TigerStyle! (Or How To Design Safer Systems in Less Time) (youtube.com).

There were quite a few really excellent points and a number of powerfully useful perspectives I’d like to capture.

Programming the negative space

If I had to pick one favorite thing from the talk, it’d be this.

The concept is to see your program like a hacker (in the pejorative sense) and try to imagine not how it should work, but how it should not work.

Joran mentions NASA’s The Power of 10: Rules for Developing Safety-Critical Code (wikipedia.org) by Gerard Holzmann.

The fifth rule is:

"5. Use a minimum of two runtime assertions per function."

By putting assertions in your functions, you’re "programming the negative space."

(Assertions crash the program when they fail. You want the program to crash because it ensures you won’t keep running with a known faulty state!)

Assertions not only ensure that your program cannot continue to run with bad logic, they also document the expectations better than any comments could.

"Now you’re showing programmers the positive space, your logic, and you’re showing them the negative space, what shouldn’t happen."

Keep your assertions in production

Every time your program runs, it’s also checking all of your expectations! It is especially important to run your assertions in production. That’s where all the weird stuff you never expected happens! And don’t you want your program to run correctly in production?

By definition, a failed assertion is a condition you cannot handle correctly.

"You’ve built this system and if you knew it’s about to corrupt tens of thousands of financial transactions every second, are you really going to say, "Let’s not shut down and continue to operate?" I don’t think so. The question is not if you shut down, but how quickly…​"

I also love his example of using assertions to check for infinite loops - unless you really do need an infinite event loop, there is always some upper bound. Figure it out and assert it. Or better yet, just use a for loop with a fixed upper bound. Also test minimum bounds - did the loop run the minimum number of times expected? Assert it!

Slow down to speed up

Take your time in the design phase.

I don’t think it’s too hard to accept the idea that time spent designing the software correctly in the abstract can be immeasurably more valuable than time spent programming the software incorrectly.

If you believe, as I do, that every line of code is a liability, then anything you can do to reduce the amount of code you have to write up front is time well spent.

Simplicity can only come from truly understanding the problem.

If you’re like me, you need to build a lot of proof-of-concept demos and tests before you really understand the problem. But this is not the same as jumping right into writing the final application!

(The book The Pragmatic Programmer has the concept of "tracer bullets" to light up the way with a minimal test program that gets from "end to end" as quickly as possible to see if you’re on the right track and get feedback as soon as possible. But the idea is to keep the "tracer code" as the skeleton of the real application onto which the rest of the functionality gets added. Likewise, most "prototype" code I’ve ever seen ends up in production in one form or another.)

Tiger Style is clearly much more deliberate about separating design from implementation. Don’t be in a rush to create the application before you fully understand it.

As Joran says:

"Don’t be hasty. An hour or day of design is worth weeks or months in production. Be like the Ents."

He goes on to explain that the TigerBeetle production had to wait for months for environmental factors. He considers this to have been very fortunate since it forced them to spend more time in the design phase, where they were able to iterate over many ideas, sketching them out cheaply.

A simple, elegant solution is hard to design, but easier to build and much easier to prove correct.

Testing

Software testing is hard. It can take decades to flush all of the bugs out of complex systems.

TigerBeetle has an incredible test suite which fully simulates the distributed environment of the system and allows speeding up time, replaying scenarios, and otherwise making finding and fixing bugs as pleasant and efficient as possible.

Not only that, the test environment can simulate every kind of network and hardware failure the system could possibly encounter, which would be extremely time-consuming and costly to try to replicate otherwise.

Since it’s loaded with assertions, the TigerBeetle codebase is constantly testing itself in the brutal testing environment.

Like the design phase, building good tooling can take time, but repay for itself many times over in efficiency later.

(I believe this matters a lot. Making something pleasant can mean more than just enjoying the process - it can mean the difference between something getting done and not getting done.)

Take time to name things correctly

We all know "naming things" is one of the hard problems in computer science.

But it’s also extremely vital. Giving things great names can save incredible amounts of time. Bad names can waste incredible amounts of time.

A good name shows that you fully understand the mental model. And a good mental model is vital for a good program.

This also dovetails perfectly with Naur’s paper "Programming as Theory Building", which I reviewed here.

Reduce surface area

Keep the number of separate components to a minimum. Try to keep the number of concepts to a minimum. Joran uses software ports as an example.

My personal examples:

FTP uses multiple ports and protocols. I assume it is a pain to program. It is a real pain to administer.

HTTP uses one port. It makes a wonderful "Hello world!" example for programming. It is relatively easy to administer.

Other points

  • Don’t allow technical debt

  • Stateless is better than stateful

  • Walk while you talk (literally, get up and move around!)

  • Statically allocate your memory to avoid difficult allocation bugs!

In a way, TigerStyle is the opposite of my dumb-first approach. But I don’t think they’re as incompatible as they first appear:

  • Both reject using the wrong abstraction!

  • Both reject large surface areas (interacting components with heavy architecture).

  • Both advocate throwing away ideas that aren’t working.

Both avoid locking you into bad ideas or being painted into corners too early.

The difference is that my "do it the dumb way first" approach would have you build the simplest possible thing that could work and see what happens.

Tigerstyle would have you keep away from the keyboard as long as possible and then go all-in on writing extremely high-quality code.

Back to programming.