My software development principles

Origin

Over the years I’ve been collating a small bullet-point list of personal software development principles. I refer to it now and then to make adjustments, but also to remind myself of the wisdom and best practices that I should be trying to adhere to. This personal development practice came up recently in a conversation with a colleague and it dawned on me that there’d been no good reason to keep the list to myself. Open Source applies not just to code, but also to collective knowledge and wisdom. So why not share the principles I believe are important?

To that end, I’m moving my note to this blog. My note was just bullet points, but each point held meaning and context to me that wouldn’t be obvious to others. To fix that I’ve significantly expanded upon, edited, and refined 10 of the principles below to make them more broadly applicable and approachable. I’ve left the extras at the bottom and will aim to elaborate on them in the same way in a future Part 2.

The Principles

1. “Keep it simple, stupid!”

KISS for short. This was the first and most memorable principle I learned on my software engineering journey. It’s the philosophy that the simplest solution to explain - the one that requires the tiniest level of mental investment to understand - that’s your prime candidate. Often we are nowhere near as bright as we think we are, so don’t over-complicate or overthink it.

2. Code lives forever

Design and execute work with an eye for your future self or others. This one comes from a wealth of experience as a code “archaeologist” in my role in porting games as well as when maintaining older versions of Unity. No matter how much you may think otherwise, your code will be read, debugged, and incremented years or even decades down the line. All unstated or untested assumptions you make can and will be a problem for future you and others.

I’ve inherited, ported, or maintained some absolute spaghetti mess projects, but I’ve also worked on some that were clean, concise, and intuitive. The former is a stress-inducing hellscape, and the latter is an open field of joy and delight. Choose wisely which one of those you want to be for someone else.

3. “Write programs that do one thing and do it well”

This one is lovingly stolen from the set of great wisdoms of Unix philosophy. I have a particular love for this principle though - It’s the idea that a program or its parts should endeavour to do just one thing and do a decent job of it. The philosophy is that a system’s elegance and strength come from its composition, not the complexity of any single part.

Writing a program or tool to do “Just One Thing” is up to a decent amount of interpretation. It fits better with the smaller core components of OS kernels and the like. Yet, extending this practice further into the software stack as much as possible can yield amazing results.

4. Sustainable development is methodical

There’s a motto Facebook used until 2014 that goes “Move fast and break things”. Sometimes it feels like the whole of Silicon Valley still abides by this. It produces products too quickly and thoughtlessly. It doesn’t account for the impact on themselves or those around them. Don’t be like Facebook.

Sustainability has two meanings here. There’s the ecological, kind to the earth and kind to each other philosophy. It can also mean building software to last, to be sustainable. Both of these require taking the slower road. Consider deeply the implications of any systems and the incentives that your software might create. Take note of when these moments occur and take your time. Be measured, sometimes literally.

5. If all you have is a hammer

Often referred to as “Law of the instrument”. As developers, we love to make bespoke, special things and come up with refined and lovingly crafted solutions. But, remember that the cleverest and best solution is often the one that doesn’t require any upfront work or future maintenance at all. Sometimes a simple workflow adjustment will do. Often, moving the solution scope back into meatspace will reveal savings across other tasks. Don’t let producers or project managers monopolize this kind of thinking. Solve the problem first, then write only as much code as the solution needed, if at all.

6. Be a cursed problem exorcist

Always ask what is being solved for and do not write any code until that’s well defined. So many guff projects have been made because of badly defined inciting circumstances, and so much time is wasted steering an already in-motion beast in the right direction. If you must start a project before the problem is defined, make a firm commitment to throw out the written work and start fresh once it solidifies.

If every solution you attempt is becoming more and more of a mess, then the problem itself could well be cursed. “Garbage in, garbage out!” is the axiom to apply here. Take a step back and try to redefine and re-evaluate the problem. Check for assumptions and biases, and explore the actual needs of the user. Be a problem exorcist and your solutions won’t inherit that curse too.

7. Leave things better than you found them

This applies to software, documentation, processes, and whatever, they all stagnate over time. Projects may not build anymore, and documentation updates may have been forgotten when changes were made. Every time you leave a project as broken as you found it, you’re guaranteeing the next person will have to do the same work you had to do to get it working again.

When you find an issue, fix it or at least report it. When documentation leads you down the wrong path or sets incorrect expectations, fix it or at least report it. When you think of a small improvement that can be made, just do it! Don’t settle for only leaving things as you found them. Water the plants, pull out the weeds, and sweep the floor. Try to make things better for the next person, as it’s more often than not going to be you.

8. Structure your work to minimize dependencies

A common problem for monolith projects is a circular node graph of ugly spaghetti dependencies that are impossible to unpick. This is an enemy of agility, quick iteration times, and easy experimentation. Do what you can when constructing work to make dependencies explicit and versioned, but also let adding dependencies be hard. Small amounts of friction added to select areas of a codebase can work wonders to discourage bad behaviour.

If in doubt, start the project entirely in isolation until the need to couple it with other work becomes too great to ignore. It’s far easier to pay that cost upfront than to unpick a tangled dependency graph further down the line.

9. A fork is a tool not a necessity, use it sparingly

To fork a project is to create a separate version from a branching point in that project’s history. Sometimes known as a stream, branch, major version, release, etc. Whatever you choose to call it, use it sparingly. Especially don’t tie your forks to something else’s forking methodology without a long hard think.

The key wisdom here is that creating a fork may solve a problem (e.g. users need a stable version but developers need a space to work quickly and fail fast). However, it also implicitly creates several problems, including:

  • Issues must be tested twice
  • Issues may reproduce across forks in the same way but for different reasons
  • Diverged codebases may make changes and fixes incompatible
  • Users moving between versions will experience issues that can compound

This is just a small subset of the issues and they compound the more forks you decide to maintain.

The vast majority of software projects will do fine maintaining only one extra fork - a release branch alongside your main. Update the release as often as possible to minimize the delta between them. Also, ensure your users take the latest releases frequently. This greatly reduces the problem space caused by creating that fork.

If you must fork again, stick to SemVer. Remember to tell marketing they can make up their fake versions all they want. The software/API should stay using SemVer, and damn anyone who tries to mess with that.

10. Stop trying to create your Magnum Opus

This is a broad reminder that it’s OK to create imperfect, shonky, or disposable work. Even when you’re striving to produce good work, it doesn’t always need to be your best. We are all still learning and growing. Your best work now will likely look ridiculous in hindsight, and that’s okay. So, don’t stress about it too much.

Another way I like to teach this is that there are, generally, three ways to work. They aren’t mutually exclusive and are all valid.

  • Growing - Experimenting, exploring, and learning
  • Producing - Putting that growth into practice
  • Healing - Taking time to heal, rest and reflect

I’ve never felt the need to have a fourth entry on this list of “smashing out the best work the world has ever seen!”. The key here is to identify which mode you’re in and let that happen. Be intentional and transition in and out of states of growth or healing as needed. Cumulative growth, healthy rest, and the lessons learned will get you there.

0. Nothing is true, everything is permitted

This last principle is stolen from The Creed. It shouldn’t be taken as a ticket to throw everything out, but a reminder to continue to question even the most foundational axioms. Another form might be the joke “To assume is to make an ass out of you and me”. Question your assumptions and others’ assumptions. Confirm past truths and build new frameworks from their ashes. Even the principles in this list don’t always apply. Break them, but remember to do so with intent and care.

Extras

  • Take measurements first
  • Write tests first
  • Don’t only fix it, understand it
  • Iteration times are everything
  • Fail fast
  • Fix the cause, not the symptom
  • Prefer composition
  • Steer the elephant, not the rider
  • Everything is documentation
  • Triple your estimates
  • Only rebase your own work
  • Commit often
  • Make doing the right thing the easiest path