I've had the good fortune of building several large-scale software systems completely from scratch over the course of my career. While many of us spend our day jobs frustrated, making what seem like tiny changes in endless piles upon piles of code, starting from an empty directory and blank IDE can be a refreshing experience. In this environment, I find it to be especially rewarding when simple milestones are hit; like another developer contributing a small feature, or a user finding some value from the creation that came entirely from somewhere inside of my brain.

But how can we design software to not end up like so many of the complex beasts that were born with good intentions? Even if you're coding in a vacuum by yourself and free from deadlines, there will always be pieces of a project that seem just a little bit too hacky for your tastes.

At almost every step in the early phases of a software project, you are one decision away from a disaster. This disaster won't end up rendering the entire project useless. Instead, it will manifest itself as tech debt. This has the potential to slow future progress down to an arduous grind; maybe not today, but at some point in the medium-to-long-term future. And when an empty directory is staring back at you, there are plenty of technical choices to be made.

  • Which language(s) do I use?
  • Which frameworks do I use?
  • How will I structure my code?
  • What will I write myself, and when will I reach for a library?
  • Once I've decided that, then which library should I use?
  • How will I configure my program?
  • How will my program communicate with others?

And these are just some of the big ones. What about all of the tiny choices that we are constantly making as we code?

  • Do I pass this variable as an argument to a function or store it somewhere more globally accessible?
  • What do I name this thing?
  • Which package does this class or function belong in?
  • Should I introduce a layer of abstraction here?
  • Should I use a switch here?
  • How will I handle this error?

While these questions may seem insignificant compared to the ones in the previous list, one must be careful. It can only take a few bad choices for your project to start heading in the wrong direction. While I'm not suggesting that your software will instantly explode into a glorious heap of flames -- in fact, your code will more-or-less work as intended -- I've seen firsthand how a string of bad decisions can slowly crystallize your software by increasing the time and difficulty it takes to add new functionality and to refactor existing logic.

Almost every successful software project that I've worked on has had at least one major rewrite in its early years. Software behaves like a living, growing organism as it evolves over time. Users (or managers) can start to demand new functionality or use-cases. Maybe you hit a scaling wall which requires a major overhaul of the project's architecture to fix. Foundational assumptions that seemed to be immutable can suddenly transform into limitations that need to be overcome. Very rarely is software finished.

So if a few seemingly small decisions can turn the experience of working on a project into something out of a nightmare, then it's important to spend the proper amount of time weighing the options at each decision point.

Enter Decision Fatigue...

With the constant decision-making that we have to do while building a greenfield project, at some point, decision fatigue will kick in. In my head, it usually sounds something along the lines of, "let's just go with this and I'll fix it later." While I can't be sure, I firmly believe that this thought is common among software developers of all ages and abilities.

Software developers around the world are collectively writing millions of lines of new code every day. And with all of this new code, comes a side dish of fatigue. Whether caused by overly strict product timelines or simply a lack of mental energy, the results of our decisions will get codified into higher level abstractions that themselves become engrained into even higher level abstractions written by someone else. And then we're stuck with the decisions that we've made. After all, no one likes a breaking change in a dependency.

I call this Design by Decision Fatigue. Our software takes the shape of the decisions that we've made in the past. And many of these decisions were simply the best ones that we could have made given the constraints under which we were working. At some point, we have to choose something and run with it. But what do we choose? How can we best prevent Decision Fatigue from ruining the software we've worked hard to build?

Combatting Decision Fatigue

From the dawn of civilization, tools have helped humans to overcome obstacles. In the case of combatting Decision Fatigue, there are several tools that I use to free up mental energy, unlock my creativity, and have fun coding instead of agonizing over what seems like an infinite number of possibilities.

Allow Yourself to Explore

When I have a seemingly important decision to make, I like to explore at least two options to prove to myself that I'm happy with my final choice. Most times, this involves writing code to get a feeling for each solution. Some people can map this out mentally, but I prefer to write out my thoughts into code. So when it's time to make a decision, even if this means deleting large swaths of test code never to be seen again, I don't consider the exploration process to be a wasted effort.

Documenting Decision Points

If I come to the conclusion that I've made a wrong decision, at a minimum, this will result in a // todo: comment in my code to document it. Or possibly a GitHub issue. For larger, more cross-cutting issues, I'll create a new git branch to fix the problem. Maybe this branch will be merged directly into main, or it might land as part of an even larger feature. The critical point is that I've recorded and persisted this decision inside my codebase, in a way that I can easily index.

Know When to Refactor

It's more of an art than a science when determining whether to a fix a perceived problem now or later. But there is a mental model to help: Ruthlessly Deliver Value.

If an incorrect decision will significantly impact the value that I can deliver over the next few weeks, then it's time to fix that choice today. Note, that this timeframe is "weeks" -- not "days", "months", "quarters", or "years". When I'm working on a project as a solo developer at a near-full-time capacity, it could very easily take a few days or weeks to properly weigh the options for the solution to a problem. And the more important a decision is, the more time it's worth investing in your discovery and thought process.

Let Tools Make Decisions For You

Much of my recent development work has been in Go, which, for better or worse, is an opinionated language. While some may not enjoy the infamous lack of error handling, implicit capitalization-as-scope, or the gofmt tool, for me, these all remove various decisions that I have to make and push the point of decision fatigue further into the future. This lets me reserve some of my energy bank for more impactful decisions.

Organize Your Code

I spend a fair amount of my decision bank thinking about package structure and code dependencies. I do my best to group related functionality in a single package, using proper variable scoping to present a logical public API for other components of my program to import and use. This acts as a value multiplier, allowing my future self to leverage the work that I've previously done. But nothing is set in stone, and if I come to the conclusion that my abstractions are wrong, I won't hesitate to refactor until I get to a representation that makes sense.

Tracer Bullets

I'm a strong proponent of the Tracer Bullet software development methodology. Tracer bullets reduce the write-compile-run-evaluate feedback loop and allow you to focus on an end-to-end solution for a given problem. This is critical for developing new projects, since a shorter feedback cycle increases the potential number of options to explore for any given decision. Sometimes it's better to just make an assumption and continue down the tracer's path, as long as you leave room for an escape hatch in case that assumption needs to change down the line.

Spend Your Innovation Budget Wisely

If time is a major constraint, prototype with tools that you already know. This reduces decision fatigue by allowing you to reuse patterns that you are already familiar with. Eventually, once the project has a more solid foundation, you will be able to replace these tools with something flashier once you reach the limitations of your current stack.

If you feel strongly about a new technology's ability to solve your particular problem, it's true that greenfield projects are often the best times to try something new. I like the idea of keeping an innovation budget. Allow yourself to explore, but don't try too many new things at once. This way you can still move your project forward while also trying out new things.

Automation is Your Friend

Automation begets more automation. As a foundation is built, you can start to think of your code as its own platform. It's worth spending time on orthogonal pieces of your platform that will decrease feature development time in the future. Even if this means pushing out the next milestone by a few days or weeks, your future self will thank you for the automation that you've added.

Know Thyself

It's important to remember that everyone is unique. Some people work best in the mornings, others late at night. Perhaps you can sling code for 10 hours a day, 6 days a week. Or maybe you prefer shorter, more intense spurts with long walks in between. Whatever your style is, it's important to know how to create an environment that will help you to thrive.

Software development is a mentally taxing profession and hobby. Take care of your mental health and give yourself the space to make sound decisions in whatever way you can. I just laid out some of the tools that work for me, but if you want to share some of your own opinions, don't hesitate to contact me to expand my own toolkit.

At the end of the day, software is shaped by decisions -- big and small. By managing decision fatigue, we build not just better software, but a better development experience.