🚙 Design by Contract, or How to Accidentally Order 27 Tesla Cars
It seems like every time some big software-related issue hits international news, there is almost always some state-oriented aspect that hints at how it was caused, and/or how it could have been prevented.
Last year, I wrote about the serious FaceTime eavesdropping bug and how it could be avoided by making implicit state machines explicit, or in other terms, just making sure that nothing unexpected can happen in any given state. And the recent Twitter hack was likely caused by some social engineering attack vector (humans, after all, are the weakest link).
Indeed, state machines are an important security model because if you can model every path of your system as a state machine and ensure that no path leads to an undesired vulnerable state, you can guarantee that your system is protected. But we'll save that for another newsletter issue.
Let's shift gears into something a little more humorous.
Accidentally Ordering 27 Tesla Cars
Last month, someone's dad on Reddit, er, "messed up" by accidentally ordering 27 Tesla cars, even though it really wasn't his fault. The TL;DR is that after attempting to make an order for a single car, a message appeared similar to:
❌ Order can't be placed because of a payment issue.
As almost anyone would do, the payment details were double-checked, and the order was attempted again. The error kept repeating itself, until finally... Success!
✅ Your order has been placed.
But, as it turns out, those unsuccessfully attempted transactions did go through (despite the error), and the result was, well, an order for 26 cars more than was budgeted for.
Now, imagine that you were developing a system like Tesla's online ordering system, where things can go wrong (as with any system). How would you prevent something like this from happening?
Furthermore, why would you ever get an "order failed" error message if the order ultimately does go through?
Design by Contract
You probably know about defensive programming, where you handle potentially unexpected inputs case-by-case. Design by contract, on the other hand, is sort of the opposite. In design by contract (DbC), you define a "contract" that must be adhered to, containing:
- a precondition, which must be true prior to execution
- a postcondition, which must be true after execution
- an invariant, which must always be true.
To illustrate the difference, I like the example in the StackOverflow discussion about the difference above. Consider a function for finding the square root of a number:
function squareRoot(number) {
// return the square root
}
With defensive programming, you assume that any number may be passed in, so you have to handle negative numbers, or even non-numbers:
// Defensive programming
function squareRoot(number) {
if (typeof number !== 'number') {
// throw error
}
if (number < 0) {
// throw error
}
// return the square root... which might be wrong?
}
With design by contract, you make sure the consumer of the function adheres to some precondition/postcondition/invariant contract (pre- and postconditions are also known as "require" and "ensure", respectively):
// Design by contract (pseudocode)
function squareRoot(number) {
// precondition
requireThat(isNumberGreaterThanZero(number));
const result = calculateSquareRoot(number); // do calculation
// postcondition
ensureThat(numberIsSquareOfResult(number, result));
return result;
}
It feels sort of like a mini-framework for making sure your code is resilient, doesn't it? And it's easy to remember:
- Require preconditions are met
- Maintain that invariants are kept throughout execution
- Ensure postconditions are met
- Return the result (if applicable)
Design by contract was coined by Bertrand Meyer in the Eiffel programming language, which is a fascinating but esoteric OOP language. I actually learned about design by contract and Eiffel while researching Sismic, a Python statechart library, which makes use of the same DbC principles.
Just One Car, Please
So how can DbC be used to prevent similar nightmares to the Tesla ordering story above? One thing that stuck out to me was the error message - specifically that the order couldn't be placed. Why would that error message ever occur if the order was ultimately (and erroneously) placed?
Thinking out loud, but perhaps something like this (again, pseudocode):
function notifyOrderFailed(order, reason) {
// precondition
requireThat(orderActuallyFailed(order));
// send response that order failed for reason
}
DbC might feel like mini-integration tests in your codebase. On the plus side, this makes the code more robust and "self-documenting" (please, write documentation anyways), but it can also add a bit of overhead to execution.
At the type-level, languages like Kotlin have support for contracts, and with TypeScript, you can have assertion functions which also helps. DbC, at least in its strict form, is not really something I've seen mainstream, though.
Still, it's a fascinating concept worth exploring, and gradually adopting, or at least I think so. Let me know your thoughts, or if you have any experience with DbC!
Some Updates
We've been working nights and weekends on XState v5, as well as a bunch of related tooling and ideas. Some of the things I'm exploring are:
- Design by contract for statecharts (surprise)
- Parameterized states (making things like step-based and dynamic flows more predictable)
- Derived values from state, including values that depend on other derived values (lazily computed)
- Automatically generating basic, fully functional UIs given just the statechart definition and event schemas (this is a really fun experiment)
- An embeddable visualizer that works anywhere (work in progress):
Drawing arrows for directed graphs is a lot more complicated than you might think! Steve Ruiz has been experimenting a lot in this area (especially with his work on statechart tools), with some awesome results. He released a library called Perfect Arrows to make it easy to draw arrows between points and shapes:
Definitely worth checking out his work.