Simplicity is the ultimate sophistication

· 6 min read

Simple things are not banal. They’re the most elegant thing available, and in software they’re also the cheapest. Dijkstra put the economics better than I can:

In the case of software, unreliability is the greatest cost factor. It may sound paradoxical, but a reliable (and therefore simple) program is much cheaper to develop and use than a (complicated and therefore) unreliable one.

At eJobs we’ve taken the platform from around 500,000 lines of code to around 150,000 so far. Nothing was lost. That’s the whole argument in one number. Earlier this year I wrote up how we picked the language for the backend rewrite.

Two ways to build

Tony Hoare:

There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies.

Hoare also invented the null reference, which is arguably the most expensive mistake in the history of computing. I assume he was applying the second half of his own philosophy that day.

Frameworks versus libraries

Both let you build on somebody else’s work. That’s where the similarity stops.

A framework takes control of your program’s flow. You extend it at the points it allows, you can’t modify it, and you learn its world rather than your language’s. A library is something you call. It composes with other libraries, it survives being swapped out, and it can even cross language boundaries.

Frameworks are hard to learn. The knowledge rarely transfers anywhere else. They limit what you can do, they multiply your dependencies, some of them get abandoned, and you spend real time upgrading your code to match new versions for no benefit you asked for. You’ll use a small fraction of what they offer. And the framework’s authors have goals that are not yours.

But everyone is using framework A and framework B, so should you?

No.

Friends don’t let friends use frameworks.

Highly available versus let it crash

Using Raft or Paxos to coordinate several instances of your code for perfect uptime is often the more fragile choice. A worker that gets restarted when it fails can beat it outright.

I’ve seen services built to be highly available that crashed because they failed to agree on who was the leader. The thing that was supposed to prevent downtime was the thing causing it. Write code that survives a dependency being briefly absent, and keep the deployment boring.

Inheritance versus composition

Prefer composition and you get loose coupling, testability, and the ability to combine behaviour from several places in languages that only allow one parent.

You also get security you didn’t have. Inheritance exposes every method you inherited, including the ones you never used and never reviewed, and callers are free to use them. With composition you know exactly which methods you’re borrowing, which is also exactly the set you need to test.

Use inheritance only when you’re certain the superclass won’t change.

Git flow versus trunk-based development

Git flow: a protected main branch, feature branches off it, pull requests, and discussions on those pull requests that are often long.

Trunk-based: everyone works on one branch with open access. Short-lived feature branches when you need them. It compiles, the tests pass, it merges. Development is actually continuous, and nobody spends a morning untangling a merge conflict that grew for three weeks.

DRY versus AHA

DRY says every piece of knowledge must have a single, unambiguous, authoritative representation. The problem is when you apply it.

Premature optimisation is the root of all evil, and DRY pushes you to optimise repetition away at the point where you understand the problem least. So start wet. Repeat yourself as much as you need to. Once it works and it’s covered by tests, any decent editor will extract the duplication for you in a minute.

You end up in the same place, except the product shipped earlier, you wasted no effort abstracting things that turned out not to repeat, and you have no bad abstraction to bend when the next case arrives. Avoid Hasty Abstractions.

Open source collaboration versus inventing your own kind

The open source model is proven. When you want people to collaborate, use the model that already works rather than a bespoke internal one.

Reward people for contributions that help others in the organisation, so that they’re motivated to get their code into a shareable state. Libraries should be able to move between teams, and between businesses.

TODO planning versus methodologies

A team-wide TODO list, plus an individual more detailed one for each person, is the simplest and most scalable planning tool I know. It needs no training, no coaching and no certification. It builds alignment, it organises priorities, and it gives you a primitive form of prediction and scope trimming when you need one.

Mostly it lets people work on the product instead of working for the methodology. Yes, this is itself a methodology. It’s just a very small one.

And yes, Scrum has failed.

Continuous simplification

After continuous integration and continuous deployment, I’d like to propose the next one.

Any intelligent fool can make things bigger, more complex, and more violent. It takes a touch of genius and a lot of courage to move in the opposite direction.

~ Ernst Friedrich Schumacher

It works at three levels and it fails if any of them is missing.

An engineer puts a properly sized piece of time into simplifying the code, the tooling and the documentation, so that future confusion and cognitive load go down for everyone including themselves.

An engineering manager hires for it. These people are typically among the slowest to produce code, because they spend the time up front on requirements and on solutions that will survive. They document. They share what they know. Hire them deliberately, because they don’t interview well against someone who types faster.

Higher management accepts that continuous change depends on the technology staying simple enough to change, and treats time to market as secondary to that. Prepare for a marathon, not a sprint.

The solution spectrum

When you have a problem, choose from this list, and prefer to be near the top:

  1. Core-language solutions
  2. Small abstractions
  3. Small helper libraries
  4. Large general libraries
  5. Frameworks

Core-language solutions age extraordinarily well. Some of them last decades. Frameworks are at the bottom for the reasons above, and should be avoided.

Fools ignore complexity; pragmatists suffer it; experts avoid it; geniuses remove it.

~ Alan Perlis

Adapted from a conference workshop given on 20 October 2022. The original write-up is gone; this is rebuilt from the slides.

#simplicity #architecture #engineering-management