Principles of software development

May 20, 2025
tech

I’ve written code which is still in widespread use today that’s old enough to be considered an adult in the UK. I’m pretty proud of that. I’ve also written plenty of code that was absolutely terrible. Sometimes these bits of code are the same. I’ve learned from that.

I’ve had the pleasure of working with some brilliant software engineers who opened my eyes to better ways of working. I’ve also worked with developers who were still learning the craft, or who didn’t care for the thing they were working on. That taught me things too.

As I write this, I’m on a plane armed with only my phone, so perhaps that will help me keep things concise, but I think it might be useful if I outlined some of my principles for software development. So, without further ado, here they are, pulled from the aether at 30,000 feet!

The best code is the code you don’t write

The advice is simple: avoid writing code if you can.

Why? Because every line of code you write is one that needs to be maintained, tested, and otherwise cared for. If you can avoid needing to do that, you can focus your limited time on other, more exciting, things.

If there’s an existing tool that does what you want, use it. If there’s a library out there which supports the features you want, go with that. If a shared calendar would work, use that instead of creating some bespoke booking system.

We all know that not everything will be a perfect fit for you. That’s fine. Sometimes you can adjust your processes or approach. Alternatively, you can try and modify or extend whatever it is that needs to be changed. And you know what? Sometimes, it might just be better to write something of your own, but that should be a last resort after considering the alternatives. As a rule of thumb: don’t write that code!

Attempting to make changes is especially important when dealing with Open Source software. If it’s Open Source, contribute upstream instead of maintaining a fork.

It may be painful thanks to whatever combination of corporate and OSS politics and personalities you need to wrestle with, but contributing back is the Right Thing to do. Most importantly (for me!) it means that you’ve improved part of the shared commons, and that allows us all to move forward. However, if an appeal to altruism isn’t something that appeals to you, contributing your patches means you won’t need to maintain a patch or fork moving forwards. Contribute enough and you can build a body of work to advertise yourself with.

Just remember, if someone else’s solution is better than yours, it’s fine to abandon your efforts for theirs. We want to solve problems, not massage our egos.

Fast feedback loops are vital

This is the biggie for me.

You can’t know if you’re heading in the right direction without feedback. The sooner you get it, the sooner you can course correct (or carry on doing more of the same). If feedback loops are long, by the time you discover something went wrong you’ve often lost the context needed to fix it quickly or cleanly.

This is one reason why I like compiled languages. There’s no faster feedback than the IDE telling you “this thing won’t even compile”

It’s also why I’ve spent the past 10 years or so working on build systems. I want the distance between finishing a thought and finding out whether it works to be as short as possible. Dan Bodart coined the term “10 second build”, and I think that’s something to strive for.

Smaller changes being pushed to production more quickly also lowers the risk of each change. Faster feedback is safer feedback too.

I’m willing to give up a lot for a tight feedback loop, but with modern tooling, distributed builds, and careful design choices, I usually don’t need to.

Assume no one reads the docs

“I’d rather spend an afternoon debugging and struggling than 10 minutes reading the docs”

Documentation is read at most twice: once when we start using something, and once when we’re really lost and can’t find the answer anywhere else. That’s certainly true for me. My experience tells me that’s true for many of you too.

This obviously impacts how I design APIs that I expect others to use. This is also why I like uniformity and consistency around how things work.

Put another way, you can rephrase this as “don’t make me think”. As someone solving a problem, I really don’t want to be taken out of the flow to deal with figuring out some weird little detail. It’s one of the reasons why I like tooling that works the same way for the same kinds of tasks across different languages.

This should also colour how documentation is written. If you assume the reader hasn’t read the rest of the documentation or the source code, but comes with a half-formed query (and probably a certain amount of frustration if they’ve been working on a problem for a while), how would you want the documentation to look? How would it best support them?

Maybe in the future this won’t matter so much. After all, the AIs have read the docs, and they’re excellent at retrieving knowledge. Perhaps the consistency I crave will be the friendly prompt of my AI assistant, one level removed from the daily grind of the tooling and APIs I regularly use.

But for now…

Favour simplicity

There’s a quote from Kernighan that states “Everyone knows that debugging is twice as hard as writing a program in the first place. So if you’re as clever as you can be when you write it, how will you ever debug it?” That alone makes a good case for simplicity.

But for me, simplicity isn’t just about debugging. It’s more than that. As someone trained to spot patterns, I’m predisposed to create premature abstractions. Favouring simplicity reminds me to resist that urge and just solve the problem in front of me.

This is good for any number of reasons, not least of which is that I tend to get done sooner; a faster feedback loop.

I think this is also part of being a good neighbour. After all, I’m sure we’ve all wandered into codebases where simplicity was an afterthought. It’s seldom a joyful experience.

Having said this, I should acknowledge the idea of “essential complexity”. Sometimes things are complex by nature. I’m okay with that, just so long as we’ve boiled away as much of the “accidental complexity” as possible. After all, complexity is the enemy.

Do the simplest thing that can possibly work

More simplicity? Yes! It’s so important, I mention it twice!

Doing the simplest thing helps rein in my tendency to over-abstract early. Abstractions reveal themselves with time. Let me repeat that: you’d need a Muad’Dib-level gift for prescience to accurately anticipate where the real seams will appear in a problem you’ve yet to solve.

When writing code, I take this to mean writing something that’ll work now, but which I know can be changed later. After all, code is infinitely plastic, and if we allow it we can reshape it indefinitely.

Now, I’ve seen this applied maliciously too as “do the stupidest thing that will possibly work”. Don’t do that. That’s the kind of thing foolish people with little care for others do.

The core idea is to give yourself options. Chris Matts expressed this well with the concept of Real Options: options have value, they expire, and you should never commit early unless you know why. You may have heard this expressed as “making decisions at the last responsible moment.”

Fear of code is a sign of where to start

You own your code – it shouldn’t own you.

If there’s a place in your code base that you fear to tread, view that as an invitation to tame the horror and deal with the complexity, not as a reason to shy away and retreat.

Fear spreads if you don’t address it. What starts as a small area that troubles us leads to bigger, more terrifying things.

I’ve seen this manifest so many times as sclerotic and moribund code bases, where people are scared to change anything lest the whole thing collapse. That leads to more bureaucracy, more caution, and delays implementing new patterns and approaches. That, in turn, kills your feedback loop.

Which, as you now know, is a thing I can’t abide.

So! Use fear as a guide for where to go next. It may not be comfortable, but it’ll be a step towards simplicity and faster feedback loops, and that’s an unalloyed good.

This principle is foundational to object-oriented programming: keep data and behavior close together. The thing is, this principle applies to more than just a style of programming.

For example, if two codebases are tightly coupled, they should live in the same repository. Yes, that might mean fewer, larger repos. That’s fine because by collecting coupled things together, we front-load the moment where we integrate them. It may make an individual change seem more painful, as suddenly all the integration points are known and need to be fixed, but all we’ve done is move those fixes earlier in the development cycle, and made them visible. Earlier fixes mean tighter feedback loops.

(See? I told you I liked those.)

Long lived code needs tests

How do you know whether a change you’ve made is safe and correct? As Nat Pryce once quipped “I can make changes really fast if I don’t have to prove they work”, so one way is to YOLO it and hope for the best.

But relying on production to tell you whether something’s broken is the longest possible feedback loop. And the bigger your system gets, the more fragile that becomes.

So: write tests.

What kind of tests? Context matters, but my rule of thumb is: the smaller, the better.

I like Google’s test size model, which I once wrote about on their testing blog. However you choose to name your tests, aim for speed and precision. Small tests run fast and isolate failures well, even if their individual coverage is limited. In aggregate, they’re powerful.

Of course, you also need some larger tests too, but it’s likely your CI will run for too long and will be prone to flakiness if they form the bulk of your testing. It’s why I’m still a fan of the testing pyramid, even though it’s a model that some people find dated.

But doesn’t writing tests mean you can’t write as much production code? While that’s true in the short term, I’m firmly convinced that a lack of tests means more bugs, which means more bug fixing, which means less time writing production code in the long term.

This suggests that all code should have tests, right? Not really.

If you don’t expect the code to live long, or for it to be an exploration of a problem you’re using to expand your knowledge, it’s quite alright not to have any tests at all. Dan Terhorst-North talks about the pattern of “spike and stabilise”, which is a useful way of figuring out which tests are needed, and as Liz Keogh points out, it’s a powerful way of getting fast feedback from stakeholders.

One last thing: I find writing tests after the fact boring and thankless. That’s why I’m a fan of TDD. Even if I throw some tests away later, starting with them helps me focus and clarifies what I’m trying to achieve.

Source control gives you freedom

I’ve been on a number of projects where dead code is kept around, sometimes in an “archive” directory, or (more frequently) still in place, untouched and unloved. This has always puzzled me. You use source control (you do use source control, right?) and that means it’s perfectly safe to delete code: it’s still there in our source control system if we need it.

Similarly, if you want to try out something new or a risky change, you can do that with complete confidence that you can go back to a known good state without needing any fancy shenanigans. A single “git checkout” and you’re back where you started.

By eliminating dead code entirely, you reduce the maintenance cost of our code (coincidentally allowing faster feedback loops, as there’s less code to compile and test) By being able to experiment freely, you can reduce our fear of the codebase, which can help you tame it.

Source control is your safety net. Use it. Trust it. It won’t let you down.

Don’t let “perfect” be the enemy of “good”

The temptation when working on a system is to want to be able to make it work for all cases, all the time. But often, solving a few cases well – without making others worse – is already a huge improvement.

Until code is in production, it has no value. So ship something. Learn from how people use it. Iterate.

A couple of examples of this spring to mind.

Meta was notorious for its mantra of “move fast and break things”. Many people focused on the second half of that slogan, but that was merely emphasising how important it was to be able to move fast. By putting something out there and seeing how people responded to it, Meta were able to nimbly adjust a project’s direction, even if that sometimes meant that not everything was working as well as it should.

Or take the example of how we generate Bazel build files for Java using a tool. To do this perfectly would have been to have special-case handling for cases that seldom occur, and to generalise for all manner of source code layouts. However, it turns out that just having anything in place offers value, and makes people’s lives better.

Don’t worry about solving every problem. Start by solving one, then take it from there. Doing so tightens your feedback loops, and that can only be a good thing.

Wrapping up

If I gave this more thought, I’m sure the list would be longer, and if I did so I am confident you’d lose patience reading this!

I’m also sure that reasonable people would make different choices, or have entirely different principles. That’s good. The gentle tension between approaches often leads to better outcomes. All we need to do is assume the best of each other.

In a way, a lot of these principles are self-reinforcing. Simplicity and not letting “perfect” be the enemy of “good” can be seen as two sides of the same coin. Having tests makes it easier to be fearless. Colocating things really does lead to faster feedback loops. That begs the question: how short could this list be? If I reduced this down to the very smallest number, I think I’d end up with:

Software development is more than just a set of principles. I am utterly convinced that writing software is a team sport and the most important thing on any software project is the people. Finding ways to work together effectively is far better than sticking dogmatically to The One True Way. Maybe that’s a blog post for another day….

But for now, it’s pretty hard to type this much on a phone keyboard while jetting through the air, so I’m going to stop here.

  rules_jvm_external's New Maven-based Resolver Less recently

rules_jvm_external's New Maven-based Resolver

April 26, 2024
bazel tech

A New Approach to CI

September 5, 2023
bazel monorepo tech

There's No Such Thing as a Free Lunch

June 12, 2023
bazel monorepo tech