Lessons from Space: Edge Free Programming

Reading Time: 12 minutes

I’m writing this blog series about what software engineers can learn from spaceflight. You can check out all the posts in the space series (as well as some other space-related code posts) right here.

We send rockets into space by igniting explosions underneath them. The explosions generate force.

A lot of force.

Let’s start with the shape of the pad, to illustrate. You might picture a launchpad as a flat slab of concrete not unlike a helicopter pad. That’s not quite accurate. Instead, the rocket sits on top of a brick wall that slopes down on either side like the Hoover Dam.

flame trench

That brick chasm is called the flame trench, and it’s where all the fire goes. Here’s how big it is (that white dot is a hard hat on top of a person):

The flame trench directs the force from the blast downward and outward in two directions. Telephone poles in front of the flame trench have to have “burner poles” erected in front of them to protect them from damage.

Then there’s the sound. During a launch, technicians pour water into the flame trench to dissipate the sound waves, which would otherwise tear the rocket apart. How much water? 330,000 gallons. 8-12x as much as you will drink in your life. 

Given the magnitude of the forces involved, you’d think the launchpad would be a wreck after every use. There are usually repairs to make, but they’re not what you’d predict.

“The hardest part of restoring the launch pad used to be the elevators. The doors for the tower elevator used to face the flame trench. We had these big, heavy shields on them to protect the standard doors inside. But the force would push those heavy shields inward, so we had to climb up to each floor and manually yank them back into place.

We learned our lesson the next time we built a tower. Now, the elevator doors face that way.”

Ken Ford points with gusto away from the flame trench as he explains this. He’s the engineer for launch pad 39B at the Kennedy Space Center. Here’s Ken smack talking an army guy (he’s a Navy vet; apparently there’s a rivalry) in front of his flame trench.

Screen Shot 2020-05-15 at 10.36.33 PM

There’s a takeaway here for software engineers. We can guard against problems with all kinds of mechanisms: tests, conditionals, tolerances, exception catching, instructions for the user, and so on.

These are our equivalent of the heavy shields used to protect those elevator doors.

Screen Shot 2020-05-15 at 10.36.13 PM
That’s the elevator tower of launch pad 39B. The brick chasm in the foreground is the flame trench. You don’t see elevator doors because they’re around the other side.

But those heavy shields weren’t necessary anymore once the engineers moved the elevator doors out of the way of the blast. What’s the software equivalent for that?

A World Without Edges

Michael Feathers has a favorite saying: “If you take care of the corners, the room takes care of itself.” He finds the quote useful for introducing audiences to the idea of Edge-Free Programming (he has done a talk on this, for which I wrote you a watcher’s guide).

michael feathers
Michael, smack talking a room full of software engineers.

One of these days, if my cheeky but relentless campaigns succeed, he’ll write a book about it. In the meantime, the idea is to identify your edge cases—your what-ifs, and exceptions, and your it-shouldn’t-do-thats—and rather than handling them, make them go away. Doing so produces a more robust piece of software.

Doing this also requires software engineers (that’s us) to identify the moments when we are negotiating with an edge and determine whether that edge deserves negotiation. We have to identify the times when we are answering the question “How should I account for X?” and consider “Can I make X go away?”

Let’s look at some examples. These examples come from an Android app that I use for teaching the Android framework to students. The app is called Canary.

Canary allows people to log their mood, and it shows them a list of their past moods. It stores mood entries in a database and preserves them across app restarts.

  • I provide students with a base version of this app, and they make upgrades (new properties, new tables) to learn about data persistence.
  • Students also use this app to learn about data visualization, because they implement some charts to show a person’s mood over time.

Here’s the thing about a teaching application. I am asking students to study and modify code in a language they have scarcely written. When you’re learning something as precise as programming, it’s really easy to get thrown off. So it’s my responsibility to streamline my code as much as possible, so I don’t accidentally throw people. It’s no longer just about code doing its job robustly. If the code I designed to teach is littered with edge cases that make it confusing, it’s not doing its job at all.

So let’s practice asking our question. “Can I make X go away?”

Example 1: How often should people log their mood?

Let’s say we’d like folks to log their mood at least once a day to collect an accurate picture of mental health. We might guard against too few mood entries with a daily notification or something. It’s tough because in software, we can’t force people to do things—we can only stop people from doing things. Lyft gets paid because you can’t book your next ride without paying for the last one.

But here’s the trap I see engineering teams fall into: we have this sweet spot in mind, and we get it in our heads that that’s the only right way to do things. We assume an edge in every available direction away from our ideal. Too few mood entries and the app can’t do its job, sure. But what about too many entries?

We could totally stop people from logging their mood more than once a day. I could set a timer and lock the form. Hm. OK, but it can’t just be a 24 hour lock on the form, because what if they log at 8PM one day and then want to log again at 11 AM the next day?

Wait a second. Why does it hurt if people log their mood more often than once a day? Why can’t we just let people log their mood as often as they please?

I tried to think of real reasons. For example, “Maybe the chart that shows their mood pattern expects daily entries.” OK, then we can choose the first entry from each day. Or, we can redesign that chart to accurately depict the time between entries and show folks a scatterplot of how they felt and when. The limiting factor on this app isn’t mood entry frequency. It’s our assumptions about what mood entry frequency should be.

In the final implementation, I let people log their mood as often as they want. Let ’em fill out the form over and over. I didn’t have a good, user experience-related reason not to.

Example 2: Clicking on a Custom Form Item

At the top of the form for logging their moods, people select one from this row of faces:

Screen Shot 2020-05-20 at 12.39.57 AM

The selections are mutually exclusive: when you click on one and then click on a different one, it should unselect the first one and select the second one.

How might we construct this code? We could:

  • Set the face we’re currently on to look selected
  • Go through all of the buttons, looking for buttons that currently look selected that aren’t the button we just selected, and unselect them.

That might look something like this:

var moods = arrayListOf<String>(
for ((index, button) in moodButtonCollection.withIndex()) {
val thisButtonIndex = index
button?.setOnClickListener({ view ->
for ((index, button) in moodButtonCollection.withIndex()) {
moods.set(thisButtonIndex, "SELECTED")
currentMood = Mood.values()[thisButtonIndex]
if (moods.get(index) == "SELECTED" && index != thisButtonIndex) {
moods.set(index, "UNSELECTED")

Alternatively, since only one face can be selected at a time anyway, we don’t really have to check which face is selected. We can just blanket unselect all the faces and then select the one that the person just tapped:

for ((index, button) in moodButtonCollection.withIndex()) {
button?.setOnClickListener({ view ->
for (button in moodButtonCollection) button?.setBackgroundColor(unselectedColor);
currentMood = Mood.values()[index]

See how much simpler that is? These two implementations look exactly the same from the UI perspective (I checked).

Of course, neither implementation allows for tapping a selected mood to unselect it. Once you’ve selected a mood, you can’t go back to not having any mood selected. We’d have to add more conditional logic to make this work.

But we don’t have a reason to do that. Here’s why: the app doesn’t allow users to make a mood entry without selecting a mood. They get a message, if they try that, to please choose a mood. So as long as the person can change their selection, they never need deselection to log their mood properly.

Example 3: Default Weather

At one point I challenge my students to add a column to the mood entries table by adding an attribute—usually a weather selector. A (very un-styled) version might look something like this:


After someone selects the weather, the weather appears in a text view on the associated mood entry in their history page (again, not styled. I’d replace it with a weather icon if styling were part of the assignment).

Screen Shot 2020-05-20 at 1.24.26 PM

When somebody makes a mood entry, I force them to choose a mood. This control is a different situation, though; I do not force people to fill out any of the other fields. It’s perfectly valid for them to submit without choosing an option for the weather.

What does this mean? It means that my form has to account for null weather. It means I need to enter something in the database for null weather. It means I have to get things out of the database with null weather. And it means I have to decide what to display for null weather.

Unless of course, I include a default option that conveys the meaning of someone not having selected the weather. Here I set a communicative default: “Unknown.”

class MoodEntryFragment : DialogFragment() {
var weather = Weather.UNKNOWN
override fun onResume() {
val logMoodButton = view?.findViewById<Button>(R.id.log_mood_button)
logMoodButton?.setOnClickListener({ view ->
if (!this::currentMood.isInitialized) {
} else {
submitMoodEntry(checkBoxList, notesEditText)
fun onRadioButtonClicked(view: View) {
if (view is RadioButton) {
val checked = view.isChecked
when (view.getId()) {
R.id.radio_rainy -> if (checked) this.weather = Weather.RAINY else this.weather = Weather.UNKNOWN
R.id.radio_sunny -> if (checked) this.weather = Weather.SUNNY else this.weather = Weather.UNKNOWN
R.id.radio_cloudy -> if (checked) this.weather = Weather.CLOUDY else this.weather = Weather.UNKNOWN
private fun submitMoodEntry(
checkBoxList: ArrayList<CheckBox>,
notesEditText: EditText?
) {
var moodEntry = MoodEntry(currentMood)
moodEntry.recentPastimes = ArrayList(
.filter { checkBox -> checkBox.isChecked }
.map { checkBox -> Pastime.valueOf(checkBox.text.toString()) })
moodEntry.notes = notesEditText?.text.toString()
moodEntry.weather = weather
Log.i("SUBMITTED MOOD ENTRY", moodEntry.toString())
val databaseHelper = MoodEntrySQLiteDBHelper(activity)

So if the radio buttons are never tapped, the default will stay “UNKNOWN.” If someone selects a weather option, the attribute is set to that option. If they then unselect the option, weather is reassigned to “UNKNOWN.” This is how a mood entry looks in the history if the person did not select the weather:

It says

Because “UNKNOWN” can be stringified for the database just like SUNNY, CLOUDY, or RAINY, no additional logic is needed to translate this option to and from the database.

Spotting and removing edges

I don’t have an ironclad strategy to give you for identifying and removing your edges. (I’m hoping Michael Feathers does, and he puts it in his book). In the meantime, Here are three ways that I do it:

  1. I look for the “why” in all of the cases of what the app shouldn’t do. Suppose I am asked to implement a feature with a “happy” path and lots of potential “sad” paths. I want to know why each of the “sad” paths has been relegated to the “sad” column. For each one, there needs to be a reason—related to user hardship—that users should not be allowed to do it. “It’s inconvenient for the developers” doesn’t count as a valid reason to permanently put something in the sad column (though it might be excluded from a minimum viable product).
  2. I examine deep conditional logic and exception catching. Suppose I’m working on code that has already been written to implement decisions about what should and should not work. I back-translate from this code to the paths that they mark and ask questions about why these paths are off-limits. Then I do the same as I do above; if I can’t find a user experience-related reason to keep it off limits, I think about what we’d have to do to allow it.
  3. I avoid null. I try not to make attributes nullable. What does it mean for this to be null? I look for that meaning, build a representation of that in the code, and set that as the default.

Believe it or not, there’s more to discuss at the intersection of launch pad design and code design.

This post is getting long, though, so I’ll leave it for another day. Tune in later for a post about the “clean pad concept” and what it can teach us about API design.

If you liked this piece, you might also like:

I mean, the rest of the space series, of course 🙂

The debugging posts (a toolkit to help you respond to problems in software)

Skills for working on distributed teams (including communication skills that will make your job easier)

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.