Code Is For Humans
Code Is For Humans
Code Is For Humans
CODE
IS FOR
HUMANS
A Guide to Human-Centric Software Engineering
BOOK I: THEORY
Code Is for Humans
by Zohar Jackson
CODEISFORHUMANS . COM
The information in this book is distributed on an “As Is” basis, without warranty.
While every precaution has been taken in the preparation of this work, neither the
author nor the publisher shall have any liability to any person or entity with respect
to any loss or damage caused or alleged to be caused directly or indirectly by the
information contained in it.
Contents
Preface vii
Introduction 1
1 Why Code Quality Matters . . . . . . . . . . . . . . . . . . . . 1
2 Strive for Your Goals, Not “Good Code” . . . . . . . . . . . . 3
3 Quality Code vs. Good Code . . . . . . . . . . . . . . . . . . 4
4 What You Will Learn . . . . . . . . . . . . . . . . . . . . . . . 5
5 Terminology . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
iii
CONTENTS iv
Postscript 122
Feedback . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
Stay updated . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
Appendix 123
1 The Zen of Python . . . . . . . . . . . . . . . . . . . . . . . . . 124
Preface
vii
viii
rial was concerned with a technique or two, such as a book about object-
oriented programming, when much more than a single technique is needed
to be a great engineer. These books often did not connect their ideas to
an overarching theory or approach to engineering. There was no material
that provided engineers with the correct framework for thinking about
engineering, while also providing the techniques and tools needed to solve
their daily problems. And especially absent from the literature was a dis-
cussion about human cognition and its effects on engineering and design.
An understanding of the effects, however, is critical to mastering those
fields.
I felt engineers needed a book to fill these gaps. This book lays out a frame-
work for understanding what code is and how software should be written.
It delineates how code is a tool meant for human minds and meeting hu-
man needs. It teaches how humans can reliably describe computations
with a complexity far beyond the capabilities of our limited monkey minds.
It explains how code can be made human-readable and human-proof. It
teaches how to balance concerns, cut the correct corners, defend against
entropy, take precautions, reduce complexity, and deal with our cognitive
limitations.
This book is the first book of a two-book series. This book, Book I, focuses on
the theory of human-centric software engineering, while the second book
focuses on the application of the theory. The first book provides theoretical
foundations that will be used to explain and support the technical examples
and techniques present in the second book. In other words, Book I is idea-
heavy, while Book II is code-heavy.
Acknowledgments
This book has been distilled from years of experience working on software,
my mistakes, and my successes. Credit for many of the ideas in this book
must be given to all the great engineers and designers who inspired and
taught me. A plethora of books, blogs, comments on Hacker News, Stack
Overflow answers, and co-worker’s mentoring have all made their way
into this book. I thank my teachers, all of whom gave me different pieces of
wisdom, and who are too many to name here.
Don Norman’s book The Design of Everyday Things and John Ousterhout’s A
Philosophy of Software Engineering are two major sources of inspiration that
provided me with language and theory that serve as foundations for much
of this book. I highly recommend you read them, if you have not done so
already.
Introduction
The most general answer is that we write code to achieve goals. These goals
might be financial, emotional, social, or whatever it is that humans desire.
The point is we are humans; humans want things, and we use code to get
the things we want.
This might seem like an over-reduction of why we write code, but it’s not.
There are useful ideas we can learn from this reduction. The first idea is
that the “goodness of code,” or “good code,” should be measured by how
well the code meets your (or your organization’s) goals.
Bad design and engineering cause more harm than you expect,
even when taking into account Jackson’s law.
1
2
This phrasing is inspired by Hofstadter’s law, which states that “It always
takes longer than you expect, even when you take into account Hofstadter’s
Law.” Both laws underscore our extreme inability to make predictions
about the future.
There are so many expected and unexpected ways that design and engineer-
ing can lead to undesirable outcomes. To make matters worse, often the
outcomes are non-linear with respect to the design, i.e. small differences in
design can result in huge differences in outcomes. Design and engineering
interact with the world in a way that is best modeled as a chaotic system.
Chapter 3 is dedicated to understanding this phenomenon.
Due to the chaotic nature of code and the large negative outcomes that are
likely to occur due to poor engineering, it is critical to study and perfect
the art of software engineering. By sharpening your craft, you can reduce
and mitigate the chaotic nature of engineering and achieve your goals more
easily.
There are no pithy programming catchphrases that will provide the opti-
mal solutions to your problem. There are no simple rules to solving the
optimization problem that is software engineering. Good judgment is what
you need. Unfortunately, judgment is very hard to teach and requires much
effort and experience to obtain. In this book, I will try to nudge you toward
developing good judgment. I will demonstrate the thought processes that
lead to better decisions, I will provide pithy phrases that are easy to remem-
3
ber and can be used when navigating the optimization problem. But at the
end of the day, it is up to you to put in the work, learn from your mistakes,
and think critically about the code you write.
Strive for code that meets your or your organization’s goals. Does your
organization want code that is cheap to maintain and reliable? Do you
want code that is easy to read and understand and doesn’t make you want
to pull out your hair? Do you need to ship this product by the end of the
day no matter what? These, for example, should be the kinds of goals that
drive your development choices. Suppose the CEO of Intel is acquiring
your startup for 15 billion dollars and gives you an hour’s notice of his
surprise visit. He is coming to your cubicle to see an impressive demo of
what he is buying, but unfortunately, there is nothing all that impressive to
show. In this instance, you’ll need to write bad code and you need to write
it fast. This happened to me during the acquisition of Mobileye.
Despite all this, striving for goals and not good code is very dangerous
advice. It is akin to telling people to not follow the law because some
laws are immoral. Most laws are, in fact, pretty good, although some
4
Striving for goals instead of code quality will often mislead one to focus
on short-term gains at the expense of long-term ones. In these cases, in-
vestments in maintainability, reliability, and extensibility are often ignored,
which will cause problems in the future. It is therefore absolutely critical to
keep long-term costs in mind when defining your goals. In fact, knowing
how to properly consider, predict, and mitigate long-term costs is one of
the main differentiators of great engineers.
You must accept that not all design decisions can be ideal, not every function
beautiful, and not every class polished. In this complex mess of a world,
not all your code can be quality, but all your code can be good.
5
The book will provide concepts, language, mental models, and principles
that are critical for developing your craft as an engineer. It will teach you
to differentiate and judge, provide you with a language for discussing code
and design, and help you determine the best way to meet your and your
organization’s goals.
5 Terminology
We can start by defining some terms.
It is important to remember that in this book, co-workers are considered users and
the code itself, not just the executable, is considered your product.
useful when thinking about parts and interactions. There are no hard
definitions of where a system starts and ends, or how its components are
to be divided. It is up to you to choose whatever is best for the problem at
hand.
Why do we need to use code as a tool to arrive at the correct binary number?
Why can’t we simply write out the number we want and be done with it?
The answer is that our minds are not built for that. We are not capable of
coming up with numbers that do what we want. Our minds have evolved
and are optimized for an environment of objects, relationships, actions,
functions, and natural language. These are the primitives of our thought
process; they are what our minds are built for and operate best with.
7
CHAPTER 1. THE HUMAN MIND 8
We operate under the delusion that our minds are general, that they can
think and reason about anything. But this is false. In fact, we are a very
specialized thinking machine, optimized for our evolutionary environment.
We are like an AI that can only play chess. We get around this limitation by
turning the whole world into the equivalent of a chessboard. We simplify
the complexities of reality into the equivalent of chess pieces. We view
reality through simplified abstractions and models.
Much of the difficulty in writing quality code comes from the difficulty of
translating the complexities of the universe into the language of our minds.
As Martin Fowler said: “Any fool can write code that a computer can
understand. Good programmers write code that humans can understand.”.
Writing code for humans is the equivalent of trying to coerce a specialized
chess AI to run general computations. One must translate the complexities
of the universe into the language of chess; into the pieces, positions, and
movements on the chessboard. Similarly, when describing computations in
code, one must use models and abstractions that are compatible with our
minds. The higher the compatibility, the better the code will be.
These zeros and ones are called machine language. Below are three func-
tionally equivalent pieces of code, in machine language, assembly language,
and Python.
CHAPTER 1. THE HUMAN MIND 9
1011100000001110000000000000000000000000101110110000101000
0000000000000000000000000000011101100001010000111111110011
0101000000000000000000000000000000001110100011111100111111
111111111111111111
mov eax, 14
mov ebx, 10
add eax, ebx
push eax
push fmt
call printf
Python
print(10+14)
Take a moment to look at the three examples and compare how they make
you feel. How did the usage of the word “feel” make you feel? A bit
surprised I imagine! Feelings are not something you imagined would be
discussed in a book about programming. Well, this is not just a book on
programming, this is a book about humans programming, and humans
have feelings.
I imagine the machine language left you feeling confused, maybe a little
frustrated, and maybe the Python made you feel relief. It was obvious to
you what the Python was doing, and you probably felt good when you saw
it and understood it.
It’s clear that the Python is far easier to understand and modify than the
assembly, and the assembly is easier than the machine language. The
difference in comprehensibility is as extreme as night and day.
CHAPTER 1. THE HUMAN MIND 10
The simplicity, beauty, and elegance of the Python should cause your brain
to scream out: “This is the way!” Indeed, this is the way. What is not
obvious is why this is the way.
Figure 1.1: Lion image vs. hex of lion image. Inspired by Understanding
intermediate layers using linear classifier probes [1].
What is it about our minds that makes the lion image and Python code much
easier to understand than hex and binary? What lessons can we extrapolate
from these simple examples? We will cover how we can sharpen our ability
to discern what the superior code or system design is. I will also show
you how to write code that is human-centric and designed for our human
brains.
CHAPTER 1. THE HUMAN MIND 11
Short-term memory of Memory remains in- Code needs to be written so that en-
∼10 seconds with max definitely with near un- gineers do not need to keep many
capacity of ∼7 items. limited capacity. items in their short-term memory
to work on the problem.
Small working mem- Unlimited working Lines of code should be short. Vari-
ory. memory. able names should be descriptive
so that their meaning does not need
to be remembered. Required con-
text should be small.
Very lossy inter- Communicate with ef- Extra care to communicate clearly
human communica- fectively zero loss. must be taken. Knowledge should
tion. be communicated in comments and
code, not email and messenger
apps, so that loss is reduced.
CHAPTER 1. THE HUMAN MIND 12
Lossy short- and long- All memory is long- Engineers should not be expected
term memory. term and permanent. to remember anything, especially
not details which are prone to be-
ing forgotten or misremembered.
Knowledge must be written.
Cognitive differences CPU differences exist, Code should be written for the low-
between individu- but differences do not est reasonable denominator. Code
als. Information is result in different end should be written so that all the
processed differently results. people who will work on it can eas-
depending on the ily understand it.
individual and their
current cognitive state.
There are two young fish swimming along and they happen
to meet an older fish swimming the other way, who nods at
them and says “Morning, boys. How’s the water?” And the
1 Occasionally errors will occur due to cosmic rays and equipment failures.
CHAPTER 1. THE HUMAN MIND 13
two young fish swim on for a bit, and then eventually one of
them looks over at the other and goes “What the hell is water?”
We live and think in the world of abstractions, yet we hardly realize it.
Humans abstract everything we encounter. We abstract physical objects,
ideas, emotions, and mechanisms. Every entity that has a name is an
abstraction in our minds.
1.4.1 Models
The title of this section, “Humans think using abstractions,” is itself an
abstraction, as it is a simplified explanation for how humans think. In fact,
it is a special type of abstraction called a model.
predator by trying to calculate all the future states of its atoms and electrons.
It is an impossible task. Even modern scientists are not capable of predicting
animal behavior from the states of an animal’s atoms. Instead, scientists use
abstractions such as neurons, emotions, and cognitive processes to make
predictions.
Models are the building blocks of science. Atoms, for example, are a
scientific model. Atoms do not exist in reality; they are merely models
in our minds. An atom is the name of a model that enables us to make
predictions about certain observed phenomena, such as why photons will
sometimes disappear (be absorbed) when they enter certain regions of
space (hit an atom), or why some things have mass and take up space.
Often a perfectly accurate model is not optimal. A more accurate model will
often come at the cost of greater complexity. For example, when teaching
children about atoms, maybe the Bohr model is more appropriate than the
Schrödinger model due to its simplicity and lack of reliance on probability
theory.
CHAPTER 1. THE HUMAN MIND 15
is usually a much better choice than using the more precise and more
complicated Einsteinian equations, which take into account relativity. The
increased precision of Einstein’s equations is unnecessary for the vast
majority of engineering tasks, while the added complexity is likely to lead
to human error and even engineering catastrophe.
we can consider the “string” as the bits output by a program and the
“universal description language” as the bits that make up the program.
Thus the Kolmogorov complexity for a given string is the length of the
shortest program that will output said string. To be clear, Kolmogorov
complexity is not restricted to bits and computer programs. The universal
description language can be a natural language and the computer can be a
human or a large language model.
Software systems are built from layers upon layers of abstractions. There are
the many hardware layers abstracting electrons and atoms into transistors,
electrical devices, logic circuits, memory, busses, and computational units.
There are the drivers and firmware abstracting the hardware components.
There is the operating system layer, which itself has many internal layers of
abstraction.
dates and then comparing their tradeoffs. The difficulty in abstraction lies
both in the conjuring up of good candidates and in comparing their relative
costs and benefits. The tradeoffs often include functionality, flexibility,
comprehensibility, extensibility, and performance. I use the catch-all term
usability to describe these tradeoffs.
There is much to say about abstraction; in fact, much of this book is implic-
itly about how to design and choose good abstractions. I am assuming you
have already studied the basics of abstraction, so we will skip the basics
and start with two often ignored topics that are critical to making good
abstractions: assumptions and familiarity.
1.5.1 Assumptions
Assumptions are critical to increasing the bandwidth of communication. If
communicating parties did not make assumptions, communication would
be tediously long and inefficient, as one would have to specify an unrea-
sonable number of details.
know what others know and even harder to know what others will assume.
Humans are biased to think that what we know is known by others. This
cognitive bias even has a name: the curse of expertise or the curse of
knowledge. You must work hard to unbias your assumptions about other
people’s knowledge. It is best to err on the side of caution and assume
your user does not have as much knowledge as you do. It is better to
over-explain than under-explain, to over-simplify than under-simplify.
1.5.2 Familiarity
We can leverage the user’s prior knowledge by creating abstractions that are
similar to those that the user is already familiar with. This enables the user
to make valid assumptions about the abstraction without needing to study
it in detail. For example, imagine the year is 1940 and you are creating the
world’s first computer. In 1940, since no one knows what a “computer” is, it
makes sense to abstract it as a composition of components such as a screen,
keyboard, processor, and memory. In 1940, everyone was already familiar
with these terms. People had seen movies, so they knew what screens
were, keyboards existed as a part of typewriters, memory was in our minds,
and processing was a term in the vernacular. These terms enabled users
to make valid assumptions about the objects they represented. Someone
would rightly assume that “memory” stores information and that it can
be read and written. They’d have intuitively understood that computer
screens are for displaying information, and that information on a screen is
temporary and changes over time. A key point here is that not only were
the names familiar, but the way the items worked was familiar. Using a
familiar name for an alien function will cause incredible confusion and
should be avoided!
already familiar with enables them to quickly make assumptions and get
an idea of what it does. If you were to name the class XDFSender, the reader
is less likely to know that XDF is a message type, even if this is incredibly
obvious to you. In such a case, adding redundancy and reducing the need
for specific knowledge is useful. XDFMessageSender, for example, would
be a better name. The marginal added verbosity is a price worth paying
for the benefit of clarity. Using names that do not assume prior specialized
knowledge, and that describe the item in terms of an abstraction the user is
already familiar with, is a great way to reduce complexity.
This example may seem extreme, but it happens all the time in the real
world. Whenever a software engineer invents something new that is unfa-
miliar, it’s the equivalent of inventing a light switch. If you can, you should
avoid inventing unfamiliar abstractions, as the harm caused is usually
greater than the utility you imagine.
The pump requested that I enter my zip code and then press enter. Unfor-
tunately, I pressed “Help” instead of “Enter.” This caused the system to
begin calling the attendant for help. I tried pressing the cancel key to no
CHAPTER 1. THE HUMAN MIND 20
avail. I had to wait a painful amount of time while the device was ringing,
waiting for the attendant to answer, before I could start over and enter
my zip code. It was both very annoying and a sign that the system was
improperly designed. Why did I press the wrong key?
If you just thought to yourself that the inconvenience I endured was due to
a user error and not a design error, then you are mistaken. All user errors
are design errors. We will discuss this further in Chapter 4, and feel free to
take a look if you are interested. For now, let’s get back to the topic at hand.
A better design, which is not deceptively familiar, would have been for
the right row of buttons to be placed as a fifth row instead of as a column.
In that case, the “yes” button can be removed as it is redundant with the
“Enter/OK” button.
I asked the attendant how often this happens, he shook his head and said
“all the time.” This is a great example of Jackson’s law. A poor design
decision made many years in the past is causing a daily nuisance to tens of
thousands of gas station attendants and customers around the world.
1.5.5 To Be Continued
As an engineer, it is your job to use the various tools of abstraction, such
as functions, classes, interfaces, variables, components, data structures,
conceptual models, and design patterns to reduce complexity.
There is much more to say about creating great abstractions, but let’s hold
off for now and continue on the topic of the mind and discuss the engineer’s
greatest foe—cognitive load.
Chapter 2
23
CHAPTER 2. COGNITIVE LOAD AND COMPLEXITY 24
NLC also fails when the variable names are not just information-less but
are counter-intuitive. For example, the code fish.bark() can be described
as a statement calling the bark function on the fish object and has an NLC
of 10. In contrast, the statement dog.bark() has the same NLC but causes
less confusion. A dog barking is a familiar idea and is not a surprising
method in a dog class, whereas a fish barking is quite strange and additional
memory must be used to remember why this fish instance is barking, and
what the bark actually means. Maybe the engineer must remember that
this is not a fish, and is really a Furry, Incognito, Small, Husky (FISH),
or maybe a “bark” is some type of swimming maneuver. The additional
memory that must be used to remember why a fish is barking acts as a
drain on our cognition and reduces our cognitive capabilities. Cognitive
load theory explains how our usage of memory affects our cognition.
The schema is cognitive load theory’s term for what we have been calling
abstractions. In cognitive load theory, cognitive load is measured in terms
of the amount of working memory used. The more working memory used,
the higher the cognitive load. Psychologists have measured the capacity
of working memory and determined that it is limited to about five (plus
or minus three) units of information, commonly referred to as “chunks.”
A chunk is a unit of information that the brain can store and retrieve as a
single entity. Chunks can consist of individual pieces of information, or they
can be more complex and consist of multiple pieces of related information
that are compressed into a single unit.
and familiarity. The participants were able to compress five binary digits
into a number between 0 and 31 1 . Note that I wrote a “number” and not
“two decimal digits.” Participants remembered “twenty-three,” not “two
three.” This is because participants were familiar with the number twenty-
three—they already had a schema for it in their minds and simply needed
to store that schema in their working memory, instead of two schemas, such
as “two” and “three.”
In machine learning, there is the concept of latent space. The latent space
is the space of all possible points that can be represented by a model. In
neural network approaches, the network learns to represent information as
a point, a coordinate, in the multidimensional latent space. For instance, in
a 4-dimensional latent space, a network may represent the concept of dogs
at point (5, 3, 100, 4). Cats, which are kind of similar (all things con-
sidered), may be located nearby at point (5.2, 3, 101, 2.7). Generally,
nearby points represent similar things.
understood as the process of finding the right coordinate and modifying our
neural connections to interpret that coordinate in the correct way. Learning
things we are already familiar with is relatively easier because our brain
knows how to interpret and work with that region of the latent space
(nearby coordinates). The learner can leverage past learning, and not need
to expend the effort of learning everything from scratch.
Intrinsic cognitive loadis the inherent difficulty of the material itself, which
is partially influenced by prior knowledge of the material. This can approx-
imately be thought of as the minimum number of chunks the material can
be compressed to, or its intrinsic complexity.
“schemas,” and thus, germane cognitive load is the load that occurs when
creating schemas.
Ideally, the writer of the code should take the burden of cognitive load
on him or herself so that the cognitive load of the user (another engineer
or end-user) is minimized. Taking the burden of cognitive load means
expending extra effort to make sure the code is less complex. The reason
the load should be shifted to the writer of the code is because engineering
is not a zero-sum game. Code is read and run orders of magnitude more
times than it is written. Thus, one person expending additional effort once
(when writing the code) results in hundreds of people expending less effort
multiple times (when using the code).
One of the reasons Jackson’s law holds true is because of this asymmetry.
Bad code will be read and run orders of magnitude more times than it is
written, causing the harm to occur far more times than is needed.
The same scaling phenomenon does not only apply to code, but to any
easily replicated item, such as songs, cars, books, and courses. There are
some sub-par Beatles songs that were written and recorded in a few hours
that have now been listened to billions of times all over the world. Think
about how much more joy would have been brought to the world if the
Beatles would have invested a bit more time and energy into producing
those songs.
CHAPTER 2. COGNITIVE LOAD AND COMPLEXITY 29
This phenomenon is called the scale law of returns. The scale law of
returns states that the return on investment for products that reach large
scales is outsized. When a product is widely distributed, the value of each
improvement (each investment) should be multiplied by the scale of the
product in order to calculate the total return.
Finally, the scale law of returns does not indicate who receives the returns.
Sometimes, the benefits of the investment are returned to the investor,
sometimes to the users, but more often than not, both parties benefit. In
the case of widely distributed products, it is important to keep in mind the
outsized benefit of additional effort and invest accordingly.
These heuristics are useful but they do not work in all cases. For example,
often something is easy to understand for one person and not for another.
What do we do then? Is the item complex or not? The best way to alleviate
this problem is to understand the cognitive biases that affect our recognition
of complexity. There are four biases that get in the way of recognizing
complexity: producer bias, the curse of cognition, the curse of knowledge,
and self-serving bias.
CHAPTER 2. COGNITIVE LOAD AND COMPLEXITY 30
at everything else. Consider yourself, and you will realize that there are
many things that you are totally incompetent at, which come effortlessly to
others. Often these differences are not due to a lack of practice, but rather
a difference in innate ability or possibly in deeply established, learned
cognitive algorithms.
This bias causes engineers to overestimate their abilities and the quality
of their code. It is more emotionally pleasant to think of our code as great
and ourselves as great engineers than to be self-critical. This is a learned
bias, as it is partially the result of a reinforcement learning process driven
by the pleasure of self-serving thoughts. Self-serving thoughts, after all,
are pleasurable, so we are reinforced to do more and more of that type of
thinking.
Due to their pervasiveness and ill effect, you must work extra hard to unbias
your estimations of the complexity and quality of your code. Without
actively unbiasing your estimations, your code will be much worse than
you think it is.
imperative to build a model of what the other person knows and what
they don’t know. How they think and how they don’t think. Without that
model, you will either be communicating at too high or too low of a level.
All good teachers and communicators do this.
Writing code is a lot like teaching. In fact, for the purposes of computation
(i.e. what the code does), code should be written so as to teach the reader
what it is doing and why it is doing it. Code should read like a teaching
lesson.
In order to write the lesson, you need to know what your student (user)
already knows. Sometimes engineers think “I know only my co-worker X
will read this code. I will write it for their skill level and knowledge.” This
approach rarely makes sense, as it runs the risk of incorrectly estimating
your co-worker’s knowledge and abilities, in addition to being wrong about
who in the unpredictable future may end up using the code. Thus, the
safest way to model your user is to imagine them as the abstract lowest
reasonably denominated user.
chunks, and two, making the process of loading information into memory
easier. The term “information” is used here liberally to refer to any fact,
detail, entity, or concept.
1. Reduce information
2. Hide information
3. Explain information
4. Compartmentalize information
Every detail, condition, and gotcha of a code base is information that has a
cost. Whether it’s a so-called “small detail” or “big detail,” it still requires
one (or more) chunks of working memory. And working memory is a
precious resource that needs to be conserved.
CHAPTER 2. COGNITIVE LOAD AND COMPLEXITY 35
Look at the JavaScript truth table in Figure 2.1 for the == operator. This
table describes the result of comparing an item in a column to an item in a
row by the == operator. The amount of gotchas and details in the table is
far too much to be easily chunked into a single chunk. The complexity of
this operator leads to many bugs. It’s so bad that the accepted best practice
is to never use the operator and simply to use the ===, which is a far less
particular, and more predictable, operator.
The reason this operator appears so complicated is because you don’t have
a schema that accounts for all the details and exceptions. And making this
schema is not a trivial task.
In Figure 2.2 is a classic XKCD comic making fun of this similarly complex
behavior.
CHAPTER 2. COGNITIVE LOAD AND COMPLEXITY 36
As Yossi Kreinin wrote: “In code and in many kinds of text, a large part of
readability is the ability to not read most of it, to quickly learn where to
look and what to skip. A big point of structured programming or modules
is the not-read ability they provide.” [8] As an engineer, you should try to
CHAPTER 2. COGNITIVE LOAD AND COMPLEXITY 37
The system was designed such that a simple interface could be used. It
was designed so that it mirrored real-world physical files and was familiar
to the users. It was designed so that the name itself, “file,” explained the
functionality and conceptual model of the system (technique #3).
Of course, beyond naming and familiarity, there are standard tools available
for explaining information such as comments, documentation, diagrams,
and videos. These should be used as secondary tools after the limits of
naming and familiarity have been reached.
The problem with complexity and complex systems is that they cannot be
understood by us, by our minds. Our minds cannot keep track of all the
details of complex systems. Complex systems are mostly in-compressible.
They resist simplification, and thus cannot fit into our working memory.
And anything that cannot be fit into working memory cannot be understood.
The complexity of biology, for example, is why the progress of biological
research is so slow and difficult.
CHAPTER 2. COGNITIVE LOAD AND COMPLEXITY 40
When you find a bug, ask yourself: Why did it happen? Was it due to
cognitive overload? Which of the principles above could have been used to
prevent this error? You may not know how the complexity can be reduced,
but that does not mean that it cannot be. Ask a talented co-worker for their
thoughts, and they will most likely point out something you missed. Learn
from bugs, as they are super-valuable lessons. Retrospection is key if you
want to level up your skills.
extreme view to the average engineer. That is because the average engineer
is not aware of the true cost of complexity. Of course, you should be an
extremist within reason. An extreme extremist will cause more harm than
good. You will get stuck perfecting code instead of making progress. It is
important that your goals are not lost to extreme extremism. In Chapter
5, we talk about balancing concerns, perfection versus progress, and the
appropriate level of extremism in regard to complexity.
Chapter 3
Jackson’s Law
Bad design and engineering cause more harm than you expect, even
when taking into account Jackson’s law.
— Zohar Jackson
When we talk about the design and engineering of software, we are refer-
ring to multiple things. These include the design of the code, user interfaces,
APIs, command-line arguments, and graphical user interfaces. Variable
names, abstractions, and conceptual models also fall into this milieu. It
includes the defensive mechanisms that prevent user errors and the correc-
tive feedback mechanisms when they inevitably, nonetheless, err. In fact,
every expression, every statement, every character, and every indentation
in a code base should be considered part of the overall design.
42
CHAPTER 3. JACKSON’S LAW 43
This reasoning is wrong. The nature of cognitive load is such that once the
mind is overloaded, cognitive performance falls off a cliff. We only have
six or so working memory slots in our minds, and it is, thus, imperative
to keep them available for the complicated parts of the code that cannot
be simplified away. If a variable is named correctly, it does not require the
use of any working memory, as upon seeing the name one instantly knows
what the variable does. The variable is thus “simplified away.” A seemingly
“minor complexity,” in this example a non-descriptive variable name, still
uses one slot of precious memory, thus making it not really minor. There is
no such thing as a minor complexity.
In some languages, there are types named i32 and f32. This convention
is not that complicated; it’s pretty trivial to understand that i32 is a 32-bit
integer type and f32 is a 32-bit floating type. But notice that we have to say
it explicitly—we have to explain what the type is. There is some translation
going on in our minds, some cognitive load. If the types were named int32
and float32 there would be no translation necessary. Maybe if you are
familiar with one of the languages, the translation is not necessary, but it is
CHAPTER 3. JACKSON’S LAW 44
As XKCD correctly points out in Figure 3.1, one goto statement, a minor
complexity, can lead to being attacked by a dinosaur.
Regrettably, both code and design are chaotic in nature, whereby even
minor alterations can yield drastically different and difficult-to-anticipate
results.
Chaotic systems are non-linear, in the sense that the magnitude of the
result of a change is not linearly related to the magnitude of the change.
A little change in a design can lead to a huge improvement in the utility
of the product, or alternatively, make the product completely useless. A
small typo can lead to a bug that causes a customer to cancel a billion-dollar
CHAPTER 3. JACKSON’S LAW 45
There have been a number of cases in history, wherein the design of pro-
tocols around nuclear weapons have either saved or almost destroyed the
world. There was an incident during the Cuban Missile Crisis in which a
Soviet submarine believed that it was being attacked, and that nuclear war
had started. Captain Valentin Savitsky decided to launch the onboard nu-
clear missiles. Protocol dictated the approval of two other officers besides
the captain. The political officer onboard approved the strike, but Vasily
Arkhipov, a lower-level officer, blocked it—thus averting nuclear war. The
designers of the protocol saved the world by requiring three, instead of
two, officers to approve the use of nuclear weapons.
Our minds are limited, and cannot consider all the ways things can go
wrong or how much harm can be incurred from each misdesign. We suffer
from possibility bias, which is the tendency to overestimate the number
and likelihood of positive or neutral possible outcomes and underestimate
the number and likelihood of negative outcomes. Possibility bias is related
to the nature of the universe, and the laws of entropy. There are simply
so many more ways for things to go wrong than for things to go right.
There are so many more ways to break something than build something.
Possibility bias is closely related to optimism bias, which is the tendency
CHAPTER 3. JACKSON’S LAW 46
for individuals to believe that they are less likely to experience negative
events and more likely to experience positive events compared to others.
Who could have predicted the huge effect design had on gas consumption?
I don’t think anyone could have predicted it. It is precisely because predic-
tion is so difficult that it is imperative to follow the best practices of design
and engineering. Best practices enable you to make good choices without
needing to make good predictions.
CHAPTER 3. JACKSON’S LAW 47
A poor design will cause a little harm to one user but a lot of harm when
you multiply it by millions of users. Of course, the same is true in reverse;
a small improvement to a design can have an outsized benefit.
Improbable uses are usages of products in ways not envisioned by the de-
signer of the product. When there are so many people using your product,
you are guaranteed that some of those people will be using it in ways you
never imagined. This happens all the time in software. APIs are so com-
monly used in ways not intended that there is a name for this phenomenon:
Hyrum’s law. Hyrum’s law states that “with a sufficient number of users of
an API, it does not matter what you promise in the contract: all observable
behaviors of your system will be depended on by somebody.” [29]
1 There are conflicting studies regarding the frequency of bit flips, ranging from hourly
to every few days. For a modern study see “DRAM Errors in the Wild: A Large-Scale Field
Study” Schroeder et al.
CHAPTER 3. JACKSON’S LAW 49
In fact, when any good’s price is fixed, and the price is below the price
which would have been set by the laws of supply and demand, then short-
ages are bound to occur.
The second-order effects of products that are used at scale can change the
fabric of society. Trains, the printing press, the internet, social media, and
phones all have had second-order effects, which are too numerous to list.
Ubiquitous products like these affect everything in unpredictable ways.
An engineering choice early in the project can force the engineer’s hand
into other engineering choices later. This is probably the most common
CHAPTER 3. JACKSON’S LAW 51
difficult deadlines and filled the code with all kinds of crap.
The only reason why this product is still surviving and still
works is due to literally millions of tests!
3. Add one more flag to handle the new special scenario. Add
a few more lines of code that checks this flag and works
around the problematic situation and avoids the bug.
6. Go home. Come the next day and check your farm test
results. On a good day, there would be about 100 failing
tests. On a bad day, there would be about 1000 failing tests.
CHAPTER 3. JACKSON’S LAW 53
8. Rinse and repeat for another two weeks until you get the
mysterious incantation of the combination of flags right.
9. Finally one fine day you would succeed with 0 tests failing.
10. Add a hundred more tests for your new change to ensure
that the next developer who has the misfortune of touching
this new piece of code never ends up breaking your fix.
11. Submit the work for one final round of testing. Then sub-
mit it for review. The review itself may take another 2
weeks to 2 months. So now move on to the next bug to
work on.
struggle in this situation is due to their memory not being sufficient to keep
track of all the components, variables, and factors on which their change
is dependent. The problem is not the complexity, the problem is that our
minds can’t handle the complexity.
The theory is named after the example stated by the authors, “one un-
repaired broken window is a signal that no one cares, and so breaking more
windows costs nothing. (It has always been fun.)”[27]
One broken window starts a chain reaction leading to more broken win-
dows. This is a very bad chain reaction to start. It’s like leaving a bit of
rust on a metal object. Once the rust takes hold, it spreads very fast. But if
the rust is prevented from taking hold in the first place, the spread will not
occur.
Beyond the broken windows effect, bad code causes harm by leading to a
loss of team morale. Working and trying to make changes in a messy code
base is frustrating and emotionally draining. Talent leaves the company,
and good engineers stay away.
3.8 Stories
3.8.1 The Laptop Bios
A few months ago, I visited my brother and his family. As usual, whenever
I visit family members, the question arises. “Zohar, can you help me fix. . . ?”
This time, my brother’s laptop needed fixing. When I powered it on, it
would start loading the bios and then abruptly restart. The laptop was
stuck in this loop. My brother had Googled the problem and called the
company’s support line. He followed the instructions to reset the bios, a
combination of pressing and holding keys, but to no avail. He was ready to
send the laptop in for repair, as it was still under warranty.
pany? How many customers decided not to buy another model from this
company?
Did the engineers who chose this alternative bios reset sequence imagine
how much it would cost the company? Probably not. It is hard to imagine
these things. But that does not mean that this was unavoidable. If the
engineers had followed design principles such as constancy, following the
path, and communicating with the user, this whole situation could have
been avoided. We will discuss these principles in the next chapter.
3.8.2 Virality
Starting a social media app is a hard chicken-and-egg problem. For users
to be interested in using the app, it must already have people using it.
Getting those first users is a challenging problem, though, because they are
not incentivized by an existing user base.
One way of solving this problem is through viral growth. Users are given
the ability to invite their friends, with the hopes of creating a viral effect.
Virality occurs when, on average, each new user successfully brings to the
platform more than one user, thus creating a reinforcing chain reaction,
until everyone is using the app. R-value is a term that signifies the average
number of new users each user brings. Thus, an r-value greater than 1
leads to the product growing virally, and an r-value less than 1 leads to the
app falling out of use. The success of a social app is often dependent on its
r-value. A high r-value will guarantee initial success.
Many UI/UX studies have shown that user sign-up rates are dependent on
the number of steps the user must take to sign up. For every extra field the
user must fill out, the sign-up rate drops by a few percent.
Now consider how the success of a social app can be dependent on the
number of steps needed to join. Too many steps, and an otherwise viral app
will drop below an r-value of 1 and fail to spread. The difference can come
down to just one step too many—one step too many, and the product fails.
Consider that a team of engineers, designers, PMs, an entire company’s
CHAPTER 3. JACKSON’S LAW 57
success can depend on just one single step being removed from a sign-up
process.
It sounds like the technical issue was relatively minor; they write that it
was due to a “mapping error.” It probably took an engineer a few minutes
to fix. But let’s consider the damage it caused. Thousands of patients
received medication that they should not have, and thousands did not
receive medication that they should have. Some patients suffered from
anxiety about receiving a bad score, and some erroneously decided to eat
less healthily because they believed their hearts were healthy and at a low
risk for a cardiovascular event. Some patients most likely suffered heart
attacks that they otherwise would not have had if they had received the
proper treatment!
Notice the outsized effects a simple mapping error can have, due to the
scale and importance of the product. I am not familiar with the code behind
the error, but if I had to, I would bet that the error occurred due to the
code being overly complex. This was most likely a case of harm caused
CHAPTER 3. JACKSON’S LAW 58
3.9 Urinals
Let’s sidetrack for a moment and discuss urinals. I apologize for the crude-
ness of the topic, but I could not pass on the opportunity—urinals are a
great example of poor design, second-order effects, and the effects of scale.
Urinals are probably the worst-designed widely used products. I could
write an entire book just about urinals, but I will spare you that pleasure.
If you go into any restroom with a urinal, you will notice somewhere
between a few drops to a pool of urine on the floor below the urinal. At
busy restrooms, such as those at airports, malls, or bars, you are almost
guaranteed a pool of urine. These pools smell bad, track on the bottom of
our shoes, and spread throughout the restroom and into the nearby spaces.
The pools are not the only thing wrong with urinals, they are simply the
most visible. Splash-back onto the user’s pants and the unhygienic nature
of the flush handle are two other first-order problems with urinals.
Let’s start with the pools of urine. There are two primary causes for these
pools. The first is caused by the gap between the user and the urinal lip.
CHAPTER 3. JACKSON’S LAW 59
Urine inevitably falls in that gap. The second cause is due to splash-back,
i.e. the reflection of urine bouncing off the urinal and onto the floor. Splash-
back not only gets urine on the floor but also on to the user’s pants.
In high-traffic areas, the urine accumulates so rapidly that janitors are hired
to constantly be cleaning the restrooms. At airports, janitors are assigned a
few restrooms and spend the day traveling between them wiping up the
urine pools.
Let’s look at the problem from a design perspective. How much harm is
caused by the poor design of urinals?
The first-order effects of the design are the pools of urine and the splash-
back onto users’ pants, both of which reduce the pleasantness of the re-
stroom experience.
The second-order effects are numerous. There are the costs of hiring janitors
to clean the urine and the extra loads of laundry that users will need to keep
their pants from smelling. More loads of washing means more pollution
in our rivers, higher energy demand, and pants wearing out much faster.
More janitors mean airports have higher expenses and higher airfare prices.
The wet floors probably occasionally cause users to slip and fall. Urine
and urea are tracked on shoes and end up on the carpets outside of the
restroom, leading to discoloration and degradation. Nothing catastrophic,
but pretty bad considering this can all be easily avoided.
further and further back. Of course, the further back one stands the more
urine ends in the pool on the floor. This leads to a destructive feedback
loop of the pool growing, users standing back and the pool growing faster.
Incentives are the primary driver of behavior, as Charlie Munger, the great
investor and hobbyist psychologist writes: ‘‘If you want to get the right
behavior, provide the right incentives.” [11]
Another way of looking at this is that the cost of peeing on the floor is
external to the user, i.e. the user does not internalize the cost of their
misuse. The way to avoid tragedy of commons situations like these is to
internalize externalities, thus aligning incentives. In this case, you would
want to design the urinal in such a way that standing further back led to
more splash-back on the user’s pants.
The same perverse incentive problems hold for the flushing mechanism.
Users have no incentive to touch a dirty, genital-bacteria-infested handle,
and so they often don’t. This leads to urine building up in the bottom of
the urinal leading to more and grosser splash-back (as it’s someone else’s
urine being splashed back).
If the designers of the urinals would have thought about the problem more
deeply and used the design principles taught in this book, the world would
be a better place. In the case studies section in Book II of this series, we will
come back to urinals and show how they should be designed.
In this book, our discussions of design primarily concern the design of the
engineering. Despite this, most of the ideas presented apply to all types of
design, including UI/UX. The ideas are mostly universal in that design is
about navigating the needs, idiosyncrasies, and capabilities of all humans.
1 One Friday night in Jerusalem, I found myself at my friend’s parent’s house for a family
dinner. I struck up a conversation with the quiet, elderly woman sitting next to me. I
mentioned that I work in software, and she in turn told me that her son does too! I asked what
type of software and she told me he made a website which he now runs. “Cool!” I responded,
“what’s his website?” “Stack Overflow, have you heard of it?”.
62
CHAPTER 4. DESIGN AND ENGINEERING FOR HUMANS 63
This list is mostly obvious, but it is still useful to state it explicitly and keep
it in mind when designing. What is not obvious is how to achieve good
designs.
CHAPTER 4. DESIGN AND ENGINEERING FOR HUMANS 64
Today the most frequent use of tar is to extract a gzipped tar archive. The
command to do this is as follows:
2‘Man pages are the go-to documentation for GNU programs such as tar.
CHAPTER 4. DESIGN AND ENGINEERING FOR HUMANS 65
Over my many years of using *nix systems, I have probably spent a few
hours of my life searching for the correct tar arguments. Every time I need
to use tar I get frustrated. Assuming my experience is common amongst
tar users, if we multiply the number of tar users by the average amount of
time and frustration they each have spent, we arrive at a very large number.
The amount of harm caused by the bad design of tar is ridiculous.
What design principles should have been applied when designing tar to
ensure this harm did not occur in the first place? The main principle
that was lacking in application here is the principle of maximizing user
convenience. The principle is self-explanatory; products should be made to
be maximally convenient for their users. The product may be a command-
line interface, an API, a user interface, a function, or even a code base.
$ untar documents.tar.gz
In fact, dividing the program into two commands and using default argu-
ments that work in the most common use cases is how GNU zip is designed,
which consists of the zip and unzip commands. You should note that there
are no XKCD comics complaining about zip.
4.1.3 Automation
In my recommended redesign of tar, the program will automatically detect
that the passed file is compressed and extract it using the correct com-
pression algorithm. Automation is a great technique that should be used
wherever possible to increase convenience. Previously, we discussed how
automation in the case of an automatic transmission reduces the cognitive
load on drivers by hiding complexity, but it also has the benefit of increas-
ing convenience. Automation reduces the effort required from users to
accomplish their objectives. Automation also reduces the likelihood of user
error; a user can’t make an error if the responsibility lies with the machine.
Finally, automation enables the designer to hide the implementation from
the user, enabling the designer to optimize the implementation at a later
date without requiring the user to learn something new, change their code,
or be negatively affected.
desires.
The tar command was built a long, long time ago in computer history;
computers were very different back then, and the needs of users have
changed significantly. Maybe the design choices made for tar were optimal
at the time of its design but are not ideal for how we use tar today. After
all, tar archives were originally used to store files on magnetic tapes (the
default storage medium before hard drives). In fact, the name “tar” itself
stands for “tape archiver.” Maybe the maintainers of tar were unable to
improve the design without breaking backward compatibility. Using tar
to untar compressed files from the internet was definitely not the most
common use case, as it is today; in fact, the internet did not even exist.
A good example of “bad design is forever” is a story from the early days of
computing written by Rob Landley.
CHAPTER 4. DESIGN AND ENGINEERING FOR HUMANS 68
You know how Ken Thompson and Dennis Ritchie created Unix
on a PDP-7 in 1969? Well around 1971 they upgraded to a
PDP-11 with a pair of RK05 disk packs (1.5 megabytes each) for
storage.
When the operating system grew too big to fit on the first RK05
disk pack (their root filesystem) they let it leak into the second
one, which is where all the user home directories lived (which
is why the mount was called /usr). They replicated all the OS
directories under there (/bin, /sbin, /lib, /tmp. . . ) and wrote
files to those new directories because their original disk was
out of space. When they got a third disk, they mounted it on
/home and relocated all the user directories to there so the OS
could consume all the space on both disks and grow to THREE
WHOLE MEGABYTES (ooooh!).
Of course they made rules about “when the system first boots,
it has to come up enough to be able to mount the second disk
on /usr, so don’t put things like the mount command /usr/bin
or we’ll have a chicken and egg problem bringing the system
up. ” Fairly straightforward. Also fairly specific to v6 unix of
35 years ago.
To add to the mess, it turns out /usr doesn’t stand for “user” but for “user
system resources.” I remember getting confused by this directory name
in my early days of using Linux. I would look for my personal files in
the /usr/ directory instead of the /home/ directory. I imagine many other
engineers also experience similar confusion.
almost forever. Most bad design is not actually forever, but some designs
have a tendency to be more sticky than others. In Chapter 5, we discuss how
data schemas, interfaces, abstractions, names, protocols, and formats tend
to have long lifespans and that more care should be taken when designing
them.
In the book The Design of Everyday Things Don Norman writes that “human
error is design error.” [15]. We will review his ideas in this section 3 .
Norman writes
When something goes wrong and a human did something which directly
caused the thing to go wrong, we have a tendency to immediately blame the
human. For example, if there is a car accident, the first person we usually
blame is the driver. Blaming the driver is easy; the driver is immediately in
front of us (availability bias) and after all, they are the ones who failed to
3 Fora better and more thorough review of his great ideas, you should read The Design of
Everyday Thing (New York: Basic Books, 2013)
CHAPTER 4. DESIGN AND ENGINEERING FOR HUMANS 70
press the brakes in time. But a slightly deeper analysis might reveal that the
street was not properly lit, the speed limit was set too high, or the driver
education system is insufficient. Blaming the driver will not solve other
drivers from making the same error; it will not do any good, or make the
world a better place. In order to prevent the error from re-occurring, we
must understand and fix the underlying cause.
The same logic holds true for your own errors. If you are having a hard time
understanding some code, it’s probably not your fault, but the fault of the
code. We could be quick and blame the author of the code, but maybe it’s
not their fault either. Maybe the system they are part of (the organization
or team) is designed in such a way, or has incentive structures, that force
the writing of bad code. A good technique for discovering who is really
to blame is called the “five whys”. The technique of the five whys was
originally developed by Sakichi Toyoda at Toyota.
Why did they make changes to production Because they thought they were actually
2
instead of staging? making changes to staging.
Why did they think they were working with Because they were confused. It was human
3
staging? error.
Why are the domains names only one Because the system was not designed
5
character different? defensively against human error.
In the above example, most people will stop at question 3. Human error
caused the problem, let’s move on. But blaming the problem on human
error is not going to prevent it from happening again. In this case, renam-
ing the two environments so that they are less similar would reduce the
likelihood of engineers making this mistake in the future. Or one can be
more cautious and require the deployer of changes to type out “production”
or “staging” to initiate the deployment.
One last thing to be said is that when designing for human error, you
should assume Murphy’s law. Murphy’s law states that “Anything that
can go wrong will go wrong.” Every possible mistake a person can make
will be made. The user might be distracted, rushed, tired, inexperienced, or
press the wrong key. Sometimes they will misinterpret a name of a variable.
CHAPTER 4. DESIGN AND ENGINEERING FOR HUMANS 72
Keep in mind the existence of optimism bias, the bias of assuming positive
outcomes and that things won’t go wrong. Murphy’s law is a great mental
tool for counteracting optimism bias.
This concept is helpful for getting you into the correct mindset and lowering
your expectations of your users. Most likely, your users are not drunk, but
they are probably distracted, rushed, or tired, which is not so dissimilar
from being drunk.
• What are the most common ways the user interacts with the product?
How can the product be improved to optimize the most common
interactions?
• What does the LRD user know and what do they not know?
That being said, as you develop a habit of asking these questions, you will
learn through retrospection and user feedback where you erred in your
answers to the above questions. You will improve at entering the shoes of
your users and, thus, improve your design skills.
I use the term futures and not future because there are many possible
futures, and all of them should be considered when designing. Each future
has a certain probability of occurring, and that probability must also be
taken into account. In the next chapter, we introduce this idea more formally
and relate the futures to a measurement of cost. We call this relationship a
probabilistic cost distribution over time and will explain it in detail later
on. But for now, let’s keep it simple.
The first (and probably most effective) technique for planning for the future
is to follow basic design and engineering principles such as simplicity,
minimalism, redundancy, and reducing cognitive load. Code that follows
these principles will be much easier to smith into whatever is needed in the
future. The great thing about these principles is that you don’t even need
to predict what the future may hold to benefit.
The next technique requires thinking. You must think about what the future
will look like and how it will affect your product. You must consider the
probabilities of each future and calculate the for each one, the cost–benefit
ratios of all the possible design choices. It takes experience to get good at
this. With years of engineering experience and actively paying attention
to when things go wrong, you will get better. Here are some questions to
prime your thinking:
4“The Yiddish rhymes and is more catchy: Der mentsh trakht un got lakht”
CHAPTER 4. DESIGN AND ENGINEERING FOR HUMANS 77
• What usage errors are users likely to make when using the product?
• In which directions will the code rot over time? What changes can be
made today to reduce future code rot?
• What future changes to the code are likely to break the current code?
Is the code barely running or is it robust to changes?
If you try to build a complex system from the ground up without letting it
evolve over time, you are almost certain to fail, and if not completely fail,
then at least produce a sub-optimal result.
The functions and classes that we create can be considered custom exten-
sions of the original programming language. Their names can be thought
of as the equivalent of reserved words. These names are words that are
not mutually agreed upon, however. The person on the other end of the
communication does not know the meaning of these words. Nevertheless,
the writer of the code has decided to use them anyway and communicate
in a made-up language.
Good names are so important because they are the primary means of
communication between engineers in a made-up language. Names should
assume that the person reading them has no idea what the entity they
represent is, so as such, they should be descriptive and specific. Descriptive
and specific names, of course, come at the cost of length and verbosity, but
this is a good tradeoff to make as long as you don’t go overboard. As an
engineer, when reading another person’s code, I prefer the names to be
long and descriptive so that I don’t have to constantly search through the
code trying to figure out what each name signifies.
4.5.1 Feedback
Feedback is critical to communication. We are so used to feedback in our
inter-human communication that it often does not enter our conscious
awareness. When talking to another person, we are constantly giving each
other feedback with facial expressions, body posture, proximity, tone of
voice, and, of course, words. We are creatures whose minds expect feedback
and are inevitably frustrated when we don’t receive it.
We all have had the experience of struggling with software that is not
working as expected and is not providing us feedback as to why. With our
code, we can often dig in and figure out the source of the issue, but when
closed-source software is lacking feedback, a theoretically trivial issue can
take hours or days to solve.
CHAPTER 4. DESIGN AND ENGINEERING FOR HUMANS 83
Feedback from software systems comes in many forms; these can be visual,
auditory, or physical, such as vibrations. Our IDEs give us instant feedback
with syntax highlighting, letting us know with red squiggly lines when
some code is problematic. Compilers and interpreters give textual feedback,
hopefully letting us know what is wrong with the code, and even sometimes
how to fix it.
4.5.2 Familiarity
In Chapter 1, we discussed the importance of familiarity and how it is
critical in reducing communication bandwidth and errors. As such, we will
only mention it here as a reminder.
The second principle states: “Explicit is better than implicit.” This principle
states that it is better for the code to be explicit in its functionality and
constraints rather than relying on implicit, hidden, or assumed behavior.
By being explicit, you make it easier for others (and yourself) to understand
what your code is doing. Being explicit is communicating; being implicit is
not communicating.
or without parenthesis:
That is not a typo in the book. That is actually what Ruby code looks like!
The compute something function is implicitly called. The syntax is such
that it is not clearly communicating to the reader what is going on. It takes
a bit of thinking and background knowledge to figure out what’s actually
happening. This syntax does not follow the principles of familiarity, in the
sense that most engineers are not familiar with a parenthesis-less function
call syntax.
You may believe that the syntaxes are only confusing to those who don’t
know the languages. You may be under the impression that the syntaxes
are not inherently confusing. You may think that the Python syntax of
using parenthesis and explicit return statements would be confusing to a
Ruby or Rust engineer.
This is the wrong way of looking at it. Good design is about reducing the
complexity of everything for everyone. The average engineer trained in
CHAPTER 4. DESIGN AND ENGINEERING FOR HUMANS 86
the average language would find these syntaxes unfamiliar and confus-
ing. Remember that minor complexities add up very quickly and that the
improbable occurs.
It’s easy to get carried away by a “good idea,” especially one that we have
come up with ourselves. The creator of Ruby probably thought it was a
great idea to not require parenthesis to call functions. He was probably
emotionally attached to his idea. He probably thought that it’s a great way
to make code more elegant and minimalistic, but really, he was deluding
himself with self-serving bias.
There are a lot of extremists in this world; there are Whabists, monks, ultra-
Orthodox Jews, anarchists, Nazis... Unfortunately, the list goes on and on.
Just as there are extremists in the political and religious worlds, there are
extremists in the engineering world. And just as extremists cause harm in
CHAPTER 4. DESIGN AND ENGINEERING FOR HUMANS 87
...
Whenever there is a battle between two sides, with extremists on both sides,
it’s usually an indication that the truth lies somewhere in between. The
world is nuanced, and humans have a tendency to build mental models
that are simple and lacking in critical nuance. Our minds like to think
about single causes producing single effects, and have a hard time thinking
about all the causes, effects, and second-order effects that exist in the real
world. As a rule, you should not trust non-nuanced opinions (especially on
controversial topics) or adopt extremist ideologies.
4.6.1 Fads
Fads are not as bad as extremism, but they should be treated with caution.
Many of the human flaws that lead to extremism also lead to fads. Everyone
wants to use the latest and greatest thing. No one wants to feel left behind
with a technology of the past.
If you decide to implement a long-lifespan project with the latest fad, you
might be in trouble when the fad passes. Suddenly the hot framework or
language you used is no longer being supported and you need to rewrite
your code from scratch.
I could add many more rows to this table, but I don’t want to make too
many enemies.
If something sounds great in theory but has never been implemented well
in practice, you should assume there is something fundamentally flawed
with it. If something is popular and old, it’s probably pretty good. If
something is popular and new, you should hold your judgment and wait it
out. Popularity is not a reliable metric for the utility of new things.
the masses. The best paths become popular and remain popular, while the
less optimal paths fade.
The idea is similar to the efficient market hypothesis, which states that as-
sets are always correctly priced given that all relevant information is freely
and readily available to market participants and is rapidly and accurately
incorporated into asset prices. If there was a more correct price for an asset,
then that would be its price. The same holds true for paths, if better paths
existed they would be the common paths.
And just as sometimes the market is not efficient, sometimes it makes sense
to follow a fad. Sometimes the masses are correct in their promotion of a
fad. For example, object-oriented programming was a fad and is now a
recommended practice. So don’t become an anti-fad extremist. If you are
going to follow a fad, do it with caution and a critical mind.
Chapter 5
The optimal solution for one project or code base is often not the optimal
solution for a different one. The optimal solution with a small budget
might be completely different from when a large budget is available. A
life-critical system, one where someone dies if the system fails, requires a
totally different level of code quality and testing than a web application.
91
CHAPTER 5. INVESTING IN THE RIGHT THINGS 92
Our focus on the present often leads us to forget our goal of keeping long-
term costs low. We are biased to rush to quick and dirty solutions, often
leading to outsized costs.
5.2 Tradeoffs
Most of the challenge of optimization comes from the difficulty of com-
paring tradeoffs. When choosing a system design or even when writing a
single line of code, you must always consider the tradeoffs of your design
CHAPTER 5. INVESTING IN THE RIGHT THINGS 93
Design and engineering require tradeoffs, there is no way around it. There
are always tradeoffs. If you are unaware of them, or do not consider them,
it is highly unlikely your design will be optimal. Below is a table of the
most common tradeoffs that engineers encounter. You can use this table as
a tool when performing tradeoff analysis.
Readability, maintainability,
Compute efficiency
development costs, simplicity
Security *
Scalability *
Cost now Cost later
Development time now Development time later
Simplicity Extensibility
Usability, maintainability, budget
Features
remaining
Automatic User control
Backward compatibility Simplicity, performance, agility
Quality, usability, maintainability,
Faster product to market
readability, performance, reliability
The tradeoffs are so different that comparing them can feel like comparing
apples to oranges. Luckily, there is a method for comparing different things.
The way to do it is to translate them into a proxy value and then compare
those proxy values. For example, we can compare apples and oranges by
how much pleasure we experience from eating them, or by their market
values.
For most projects, the proxy value that is best for comparing tradeoffs is
monetary value over time. Even if no actual money will be exchanged,
monetary value can still be used. For example, on volunteer open-source
projects, monetary value can be calculated in terms of engineering time,
utility to users, and goodwill.
Computing monetary value is not trivial, and it is made extra difficult due
to the unpredictable nature of the world. The world is far too chaotic to
know what the outcome of a decision will be. In some cases, a decision
may only incur the initial development costs, and in others, it will lead to
disaster and the bankrupting of a company.
Of course, since we don’t know what will happen in the future, we can’t
know what the actual costs of each decision will be. What we can do,
however, is construct a probabilistic cost model, or PCM. A PCM considers
a range of possible outcomes, their relative probabilities of occurring, and
their individual costs. In expectation, or on average, such a model will
provide an accurate estimation of future costs.
There are, of course, many other outcomes that may occur. It is impossible
to consider every possible outcome, since there are an infinite number
of possible magnitudes for each point in time. Rather, we consider each
category of outcome and its probability of occurring. There is the category
where things go well, there is the category where disaster strikes, and there
is a category of somewhere in between. For each category of outcome, we
create a graph that contains the average values of that category.
Let’s look at a few simple examples so you can get a better idea of what
PCMs and COGs look like.
CHAPTER 5. INVESTING IN THE RIGHT THINGS 96
A Leaky Roof
Fixing a leak in a roof induces a small cost today, but it will save tens of
thousands of dollars in future structural damages. Assuming that the cost
to fix the leak is less than the cost to fix the future structural damages, 1 it
makes sense to fix the leak today.
Figure 5.3: Fix leaky roof now Figure 5.4: Fix leaky roof later
Figure 5.5: Cost per vehicle with the Figure 5.6: Cost per vehicle with the
expensive part cheap part
1 More accurately, you want to consider the discounted cost of capital, but since finance is
out of scope for this book, let’s just keep it simple.
CHAPTER 5. INVESTING IN THE RIGHT THINGS 97
Figure 5.7: Cost per vehicle with the cheap part and a vehicle recall.
Quick and dirty (Q&D) is a development style where very little time is spent
planning, designing, and creating the product. Engineers simply write
whatever comes first to their minds without considering the consequences
and long-term costs. In Figure 5.8, we see the typical cost outcome graph
of Q&D.
As time goes on, bug and new feature costs will appear to the right. Notice
how the cost of new features and bugs are more than the initial develop-
ment costs. Notice that the baseline cost goes from zero to a small value.
This is the cost that comes from this feature’s complexity increasing the
development and maintenance costs of other parts of the project.
CHAPTER 5. INVESTING IN THE RIGHT THINGS 98
Sometimes engineers get lucky and Q&D ends up not being all that expen-
sive. Figure 5.9 represents such a scenario. This type of scenario is more
likely to occur on small, low-complexity projects with a single maintainer.
On more complex projects, quick and dirty often turns out to be not so
quick and very dirty. If the complexity is too great, then building an initial
quick solution will often end up taking longer than attempting a quality
solution. In the disaster scenario in Figure 5.10, notice the cost of the bug is
off the chart. Unfortunately, this scenario is not that uncommon; bugs like
this typically include costs related to lost customers, damage to property,
or downtime.
This is the scenario where code is designed and written carefully. In Figure
5.11, notice that the initial development costs are slightly larger than in
the quick and dirty scenario, but new features and bugs are less expensive.
This brings the total cost of this outcome graph to be lower than the quick
and dirty scenario #1.
Sometimes you get lucky; there are no bugs and all goes smoothly as shown
in Figure 5.12. When designing carefully, you are much more likely to “get
lucky.”
Even so, when designing carefully, the average magnitude of bad things is
lower than when practicing Q&D. Note that the cost of the disaster scenario
in Figure 5.13 is much less than the total cost in the quick and dirty disaster
scenario.
Negative Costs
For simplicity’s sake, I have so far not discussed negative costs. In theory,
cost outcome graphs should dip into the negative when an outcome is
beneficial or profitable. Personally, I find that including negative cost tends
to add unnecessary complexity to the model. I believe it is much easier to
think about costs and benefits separately. Benefits are less chaotic in nature,
and thus, we gain less by modeling them with a PCM. A simple Gaussian
distribution or a long-tailed distribution is sufficient for modeling most
benefit cases.
Unless you have some uncanny ability to think about negative costs, or the
nature of your specific problem requires you to include negative costs in
your model, I recommend you calculate the costs and benefits separately.
The Pareto principle applies to software in many ways. For example, 20%
of the code and functionality often provides 80% of the value, or 80% of a
project’s bugs are caused by 20% of the code.
other words, this means that at some point, increasing the amount of a
particular resource used in production will not result in a proportional
increase in output.
Peter Norvig suggests how to deal with the law of diminishing returns in
Coders at Work.
CHAPTER 5. INVESTING IN THE RIGHT THINGS 103
There are 100 other things you could be doing that are just at the
bottom of the curve where you get much better returns. And at
some point you have to say, “Enough is enough, let’s stop and
go do something where we get a better return.” [18]
5.6.1 Lifespan
Investments in products that have a long lifespan will have outsized yields.
This is apparent from looking at a PCM of a long-lifespan product. A poor
design choice will shift the entire distribution upward. If the product’s
lifespan is long, then even a minor shift has a large overall effect. In the
realm of software, long-lifespan products are typically schemas, interfaces,
division of components, names, protocols, specifications, and formats.
Consider how long TCP/IP, JPEG, and HTTP have been around. Products
which are specifications are particularly long-lived because there are many
different implementations of the specification and a large number and
variety of users.
It follows that code that has a short lifespan does not need to be invested
in as heavily. If you are just writing a script to parse some data which will
then be thrown away, it probably does not make sense to be a perfectionist
about it. That being said, you must be confident that the code will in fact
have a short lifespan. Often, code we think will have a short lifespan lasts
for a long, long time.
5.6.2 Scale
Similar to products that have a long lifespan, products that have or will
have a large number of users or usages have a high return on investment.
The number of usages is a type of “scale” and, thus, abides by the scale law
of returnsdiscussed earlier.
5.6.3 Foundations
A unit of code or data that is foundational to the system, program, or com-
pany should be invested in heavily. Similar to a house, if your foundation is
not built well, your whole house will collapse, and all investment in other
aspects of the house will be wasted. Foundations typically consist of the
system architecture, division of components, schema, and abstractions.
opinion, but the point remains. Premature optimization is very bad. Let’s
review some of the reasons:
Bernie Cosell, one the one of pioneering engineers of the internet, said:
It’s generally better to wait until you have collected data and identified
actual performance bottlenecks before trying to optimize your code. Write
your code as lucidly, simply, and clearly as you can. And then, if it needs
to be sped up later, you can optimize it later. In general, it’s better to pick
a design that fits your data and mental models than the one that is the
fastest. Don’t fall for the temptation to show off how smart you are by
implementing the “double reverse backwards pointer thing;” it will just
waste your and your co-worker’s time. And if your co-workers are worth
CHAPTER 5. INVESTING IN THE RIGHT THINGS 106
their salt, they won’t be impressed by your “ingenuity;” they will think
you are a noob.
5.8 Featuritis
Knuth believes that premature optimization is the root of all evil, but I think
featuritis is a greater evil.
Users, product managers, and even engineers are often under the delusion
that more features are better. The main issue with too many features is that
it makes products too complex. As Don Norman writes:
Apple is famous for successfully fighting feature creep. On the iPhone, for
example, many basic features are not supported. Users can’t add more
than four items to the dock, icons can only be placed at specific locations
on the home screen (they are snapped into place), and icon sizes cannot be
modified. Limitations like these reduce the complexity of the interface and
make iOS a more usable product for most users. It can be argued that much
of Apple’s success comes from its obsession with reducing complexity.
The idea that adding more features does not increase the utility of a product
is often called “worse is better”. At a certain point, having less functionality
(“worse”) becomes the more desirable choice (“better”). Software that is
limited in functionality and, thus, complexity, is often more appealing to
the users than the opposite.
If you determine that the feature is not worth implementing, just say “no.”
Saying “no” is part of your job as an engineer. You were not hired to be a
feature monkey and destroy the code base with featuritis. You were hired
to help the company achieve its goals, and saying “no” is often the best
way to do that.
Jeff Geerling writes in an article titled “Just say no” that as an engineer, you
should “Be liberal with your ‘no’, be judicious with your ‘yes’.” [6]
5.9 Over-Engineering
Over-engineering in software development refers to the practice of cre-
ating overly complex and unnecessary solutions to problems. This can
occur when engineers implement excessive or unnecessary features, use
unnecessarily advanced technologies, design overly complex architectures,
or use excessive abstraction.
Stack Overflow is one of the most popular websites in the world, receiving
tens of millions of page views per day. A product like Stack Overflow must
have high availability and be able to deal with massive amounts of traffic.
Stack Overflow is not a trivial application; it has a bunch of functionality,
insane traffic, and a huge amount of data. ByteByteGo created a great
graphic (Figure 5.16 that uses Stack Overflow’s architecture as an example
to demonstrate engineers’ propensity for over-engineering.
CHAPTER 5. INVESTING IN THE RIGHT THINGS 109
all possible use cases, over-engineering can have several negative conse-
quences:
• Increased risk: The more complex the software, the higher the risk of
bugs and security vulnerabilities. It’s harder to test and ensure the
correctness of overly complex systems.
5.10 Minimalism
Minimalism is an excellent antidote to over-engineering, featuritis, and
complexity. As an engineer, you should strive for minimalism in your
products. To be clear, minimalism does not mean neglecting to write com-
ments, documentation, or using short variable names. Rather, minimalism
means not using the big fancy thing if the simple thing will do. Not using
bloated frameworks, third-party libraries, and complicated design patterns.
CHAPTER 5. INVESTING IN THE RIGHT THINGS 111
When I interview frontend engineers, I ask them to build a web page that
consists of a few buttons, and some text that is to be displayed depending
on the timing of when the buttons are clicked. I won’t get into all the details,
but it’s not that complex. When done properly, the whole thing requires
maybe 25 lines of plain old JavaScript and a few HTML elements. In my
experience, 90% of the candidates, those that have already passed an initial
phone screen, end up failing this assignment.
I let the engineers use whatever tools, frameworks, and languages they
want; they are allowed to use Google, Stack Overflow, etc. Most engineers
choose to use a bulky frontend framework; some choose to use TypeScript,
and then build scripts to transpile the TypeScript to JavaScript. Others
use a boilerplate template complete with Node.js and all the various build
and deploy configurations. These engineers nearly always fail. They get
stuck installing packages, getting their configurations to work, fixing URL
CHAPTER 5. INVESTING IN THE RIGHT THINGS 112
routing issues, etc. If they do succeed in getting all the bloat to work, often
there is a bug in the functionality, i.e., the buttons don’t function in the way
that I had asked for. Often, this is because the bloat has added complexity,
making the functionality harder to implement, or because they have spent
so much time setting everything up that they did not have time to invest in
the functionality.
The middle way applies not only to religion but also to engineering. It is a
great heuristic for guiding engineering decisions.
In the book Surely You’re Joking, Mr. Feynman! [4], Feynman retells a story
of using the middle way to select gears for a mechanical computer. During
WWII, before he worked on the Manhattan Project, Feynman was assigned
to building mechanical computers for calculating artillery trajectories. The
computers consisted of many interconnected gears which would turn to
perform calculations. Feynman’s manager gave him the following advice
on how to select gears.
There are two rules you need to know to design these machines.
CHAPTER 5. INVESTING IN THE RIGHT THINGS 113
Second, when you have a gear ratio, say 2 to 1, and you are
wondering whether you should make it 10 to 5 or 24 to 12 or 48
to 24, here’s how to decide: You look in the Boston Gear Catalog,
and select those gears that are in the middle of the list. The ones
at the high end have so many teeth they’re hard to make, if they
could make gears with even finer teeth, they’d have made the
list go even higher. The gears at the low end of the list have so
few teeth they break easy. So the best design uses gears from
the middle of the list. [4]
In this story, we see the power of the middle way. Even without knowledge
of the gears’ physical properties, the middle way enabled Feynman to make
good engineering decisions.
Let’s step back for a second, and consider why there was disagreement
between the MIT and Bell Labs engineering teams. How can two groups
of talented and brilliant engineers come to opposing conclusions? Maybe
there are two ways to the optimal solution? Or maybe one team was simply
wrong?
There are probably many partially correct answers as to why the two teams
disagreed. One answer is that biases, groupthink, and familiarity led the
teams to diverge into different development styles. Another answer is
that the teams had different goals, different users, and different resources.
Maybe MIT had more students and inexperienced engineers using the in-
terfaces, so it was critical that the interfaces be as simple as possible. Maybe
Bell Labs, a more commercial entity, had strict deadlines and needed to get
things to work quickly to meet deadlines, so implementation simplicity
was prioritized over correctness and interfaces.
Yet again, we see how engineering choices should take context into consid-
eration. When you find yourself being exposed to an engineering dogma,
consider in what contexts (if any) does the dogma make sense.
Personally, I think that both MIT and Bell Labs are justified in their position
on interfaces versus implementation. On projects where there are many
interfaces and those interfaces are widely used, then making sure those are
simple is the priority. On projects that are small, maybe personal projects, a
simple implementation may be a better choice as it may not be worthwhile
investing in simplifying the interfaces at the cost of complicating the im-
plementation. If you are the only one working on the code base, it’s not a
problem for you to deal with slightly clunky interfaces, since you are the
creator and most likely understand how to use them.
For most cases, I believe worse is better is preferable. It is better to trade off
correctness, consistency, and completeness for simplicity. 2
2 It should be noted that consistency does lead to simplicity. Consistent things have fewer
details, and are easier to compress. This is the main reason that consistency is a good practice!
CHAPTER 5. INVESTING IN THE RIGHT THINGS 116
Being a great engineer requires you to keep your mind open and consider
the taboo, be iconoclastic, and do not fall prey to mind-destroying dogma
and extremism.
Chapter 6
Be a Mensch
Being a mensch will not only elevates your products, but also your life.
Others will be drawn to collaborate with you and entrust you with greater
responsibilities. Colleagues will mirror that same integrity back towards
you. Doors will be opened for you. You will earn respect and admiration
and make the world a better place. Being a mensch will lead you to a
happier, more meaningful, and more joyous life.
• Going out of their way to help others, even when it may not be
117
CHAPTER 6. BE A MENSCH 118
• Treating others with kindness and respect, even if they are not kind
in return.
• Being humble and not seeking credit or attention for good deeds.
Let’s consider some of the most important ways in which being a mensch
applies to software engineering.
Giving the benefit of the doubt and making an effort to not succumb to the
fundamental attribution bias is imperative to being both a great engineer
and mensch. As we discussed in Section 4.1.3, it’s best to not jump to
conclusions and assume another person’s code is garbage or that another
engineer is incompetent or lazy. One must withhold judgment until one
understands the context in which the code was written.
Internalize Externalities
Being a mensch benefits you just as much as it benefits those around you.
When you display mensch-like qualities, it often encourages others to do
the same, creating a virtuous cycle. People will start reflecting your values
back to you, building a culture of mutual respect and integrity. The trust
you garner will lead to you being entrusted with more responsibilities.
Collaborations will become more frequent and fruitful. Opportunities will
present themselves more frequently. Doors that once seemed closed or
inaccessible will open.
Wrapping up Book I
In the large and complex world of engineering, it’s easy to lose focus and
become lost in fads, ideologies, approaches, and misguided optimizations.
It is critical to remember that these are just means to an end. It is critical
to remember that engineering itself is just a means to an end, and that end
is always a human goal. To be effective engineers, we must strive to keep
these goals, our terminal objectives, in mind and not get lost in the many
distractions along the way.
121
Postscript
Thanks for reading! I hope you enjoyed this book and learned something
valuable.
• Chapter 5: Testing
• Appendix
Feedback
If you have feedback feel free to email me at
[email protected]. I would love to receive engineering stories
and educational examples that could be used in the next edition or Book II.
122
123
Stay Updated
You can join the newsletter to receive updates as chapters are updated and
released. Sign up at https://codeisforhumans.com
The Zen of Python is included in Python as an Easter Egg. You can run the
following in a Python interpreter:
124
125
126
5.11 Design and build carefully #1. A typical outcome. . . . . . . 99
5.12 Design and build carefully #2. A lucky outcome. . . . . . . . 99
5.13 Design and build carefully #3. Disaster strikes. . . . . . . . . 100
5.14 Diminishing returns. [2] . . . . . . . . . . . . . . . . . . . . . 102
5.15 Over-engineering tweet [17] . . . . . . . . . . . . . . . . . . . 108
5.16 Stack Overflow architecture from ByteByteGo [19] . . . . . . 109
5.17 XKCD The General Problem #974 . . . . . . . . . . . . . . . . 111
List of Tables
127
Bibliography
[5] Dr. David Geddes. Advice from nhs to practices affected by qrisk2 it
error. Pulse, 2016.
[7] Richard Hamming. The Art of Doing Science and Engineering. 2020.
[12] Charlie Munger. 2007 usc law school commencement address. https:
//www.youtube.com/watch?v=jY1eNlL6NKs, 2007.
128
BIBLIOGRAPHY 129
[13] Ann Murphy. Calls for action on vacant housing with just 716 homes
for rent across the country. Irish Examiner, 2022.
[27] James Q. Wilson and George L. Kelling. Broken windows. The Atlantic,
1982.
132
INDEX 133