Manifesto
Software Engineering: Strong Opinions, Loosely Held
I’m not handing down commandments from Mount Stack Overflow. These are principles I’ve picked up by building software, making mistakes, changing my mind, and sometimes spending far too long fixing problems I created myself.
Some of them might contradict each other. Good. Context matters. If experience proves me wrong, I’ll change my opinion. Preferably before production does it for me.
Contents
Section titled “Contents”- 1. I’m a builder, not a coder.
- 2. Favour independence over dependencies.
- 3. Measure thrice, cut once. Prototype first.
- 4. Don’t blindly follow Uncle Bob.
- 5. Every configuration should have a sensible default.
- 6. Null is not a business strategy.
- 7. Not everything needs an interface.
- 8. Don’t build a cathedral to house a garden shed.
- 9. DRY is advice, not a blood oath.
- 10. Don’t make me dissect your code.
- 11. A string is not a state machine.
- 12. Make it good. Make it better. Make it lean.
- 13. The cloud is just somebody else’s computer. And invoice.
- 14. Treat me like an adult. I can read the code.
- 15. Don’t sweep errors under the carpet.
- 16. If it ain’t broke, don’t rewrite it because you’re bored.
- 17. Write code for future you. He’s a judgemental bastard.
- 18. I’d rather fail at building something than fail to build something.
- 19. Automation should save effort, not become a second job.
- 20. Choose tools for the problem, not the hype.
- 21. Learn the language, not just the framework.
- 22. AI is my power drill, not my architect.
- In the end
1. I’m a builder, not a coder.
Section titled “1. I’m a builder, not a coder.”“I love it when a plan comes together.” — The A-Team (2010)
Saying I like writing code is a bit like saying a mechanic enjoys unscrewing bolts, or a sculptor enjoys hitting things with a chisel. It’s not entirely wrong, but it rather misses the point.
I don’t particularly love writing code. I love building things. Code is one of the tools I use to do it.
That’s why I enjoy getting under the bonnet: taking things apart, working out what makes them tick, and occasionally trying to build something similar myself. It’s not necessarily because I think I can do it better. Sometimes I’m simply fascinated that somebody made it work in the first place.
An architect doesn’t become an architect because they enjoy drawing pretty diagrams. They want to see something they’ve imagined become real.
I don’t write code for the sake of writing code. I write code because I want to build something.
2. Favour independence over dependencies.
Section titled “2. Favour independence over dependencies.”“You’re gonna need a bigger boat.” — Jaws (1975)
It’s remarkably easy to turn a tiny application into a 700 MB monster. A library to parse arguments, another to colour the terminal, something else to read input, and an entire web framework to serve “Hello World”. Don’t you remember dial-up?
Or it’s Thursday evening, 9 pm, and you’re watching CI download 700 packages because somebody needed an enum of HTTP status codes, or an enormous ORM to query two tables.
Then a security vulnerability makes the news. You relax because you haven’t installed that package. Except you have, indirectly, six dependencies deep.
Libraries are useful. I’m not suggesting we all start implementing cryptography from scratch. But every dependency brings maintenance, security, compatibility and deployment costs. It should earn its place.
After all, we’re only one npm install is-even away.
Import a solution, not an entire new set of problems.
3. Measure thrice, cut once. Prototype first.
Section titled “3. Measure thrice, cut once. Prototype first.”“Houston, we have a problem.” — Apollo 13 (1995)
Prototypes come in all shapes and sizes. Sometimes it’s an architectural experiment. Sometimes it’s a scratch project to prove an algorithm. Sometimes it’s a quick demo to gently show stakeholders why their brilliant idea might not be quite so brilliant.
I love building software, but my goal is to write as little code as necessary. A couple of hours proving an assumption can save weeks of building an entire ecosystem, only for a product manager to ask, “Why doesn’t it do that?”
A prototype doesn’t have to be pretty, reusable, or production-ready. It needs to answer the question you built it to answer. Then you can decide whether to keep it, throw it away, or try something else.
Code is relatively cheap to write. Building the wrong thing is expensive.
4. Don’t blindly follow Uncle Bob.
Section titled “4. Don’t blindly follow Uncle Bob.”“It’s a trap!” — Return of the Jedi (1983)
“Can you review my PR?”
“Of course, but hasTimedOut isn’t descriptive enough. What’s timed out?”
“The network request. It’s inside getNetworkRequest.”
“Then call it hasNetworkRequestTimedOut. Also, that function should really be a class behind an interface, with dependency injection, so other services can reuse it.”
“It’s 40 lines of code, it’s being replaced next week, and there’s a critical production bug bringing down the network.”
“Sorry, doesn’t meet our coding standards.”
Robert C. Martin has contributed plenty to software engineering. That doesn’t make his books religious texts. SOLID, Clean Code, and design patterns are tools for solving problems, not excuses to manufacture new ones.
The same goes for every so-called best practice. Understand the reason, check the context, and use your own judgement. If your interpretation of clean code is preventing a critical fix from reaching production, perhaps the code isn’t the problem.
Take the wisdom. Leave the worship.
5. Every configuration should have a sensible default.
Section titled “5. Every configuration should have a sensible default.”“Life finds a way.” — Jurassic Park (1993)
There’s nothing quite like deploying a container, watching it fail, and spending half an hour trawling through logs only to discover that an environment variable wasn’t set. Fix it, rebuild, redeploy. Marvellous.
Years of “Config not found” errors have convinced me that optional configuration should come with sensible defaults. Defaults reduce defensive checks, make behaviour predictable, and give everyone the same baseline for testing.
If an optional setting is missing, I’d generally rather an application worked with reasonable behaviour, even if it’s not quite what someone intended, than fell over immediately.
Obviously, secrets, security settings and genuinely required configuration are different. Failing clearly is better than silently doing something dangerous or corrupting data.
Configuration should customise the application, not be an obstacle course before it starts.
6. Null is not a business strategy.
Section titled “6. Null is not a business strategy.”I’ve spent enough years dealing with null reference exceptions to develop a rather personal dislike for them. Learning Rust has only reinforced the feeling.
Too often, a function returns either the thing we asked for or null, and then every caller, and every caller above that, has to check whether something exists. Half the codebase ends up checking whether the other half remembered to return anything.
If all I need is to know whether a record exists, give me a deliberate existence check. If absence is an expected result of a lookup, represent it explicitly, perhaps with an Option, a discriminated union, or an equivalent. And if something that must exist suddenly doesn’t, that’s an error, not an invitation to return null and hope for the best.
I’m not campaigning to abolish null. Nor am I suggesting a separate existence query before every fetch; that’s sometimes wasteful and can introduce races. I’m arguing for honest contracts.
Missing data is a possibility. Broken assumptions are errors. Stop pretending they’re the same thing.
7. Not everything needs an interface.
Section titled “7. Not everything needs an interface.”I like interfaces. Really. The repository pattern is excellent when you need to swap implementations. Factories are brilliant when different inputs genuinely need different behaviour.
What I don’t like is the apparent belief that every class must have an IWhatever twin because somebody once read about dependency inversion.
Then there’s service drilling: a class implementing an interface, injected into another class implementing an interface, injected into yet another class implementing an interface, all so we can eventually call a little function that could’ve lived quite happily in a static utility class.
If there’s one implementation, no useful boundary to protect, and no realistic benefit from substitution, ask what the interface is actually achieving. Sometimes what you wanted was a namespace and a function.
Abstractions should provide flexibility, not the illusion of it.
8. Don’t build a cathedral to house a garden shed.
Section titled “8. Don’t build a cathedral to house a garden shed.”“…whether they could, they didn’t stop to think if they should.” — Jurassic Park (1993), abbreviated
I’ve been guilty of this one myself. Spectacularly.
When I was the only developer on Country Eats, I convinced myself we needed five APIs, three frontends, a handful of microservices, SQL Server, MongoDB, Redis, RabbitMQ, and a Kubernetes cluster. Very impressive. Very enterprise. Very unnecessary.
I got excited about building architecture worthy of a huge tech company when a single API, frontend and database would’ve done the job. It delayed release, made features harder to add, and turned changes into regression-testing expeditions. Worse, I was the only one who knew how the whole lot fitted together. If I’d been hit by a bus, we’d have been screwed.
My favourite irony? I brought in RabbitMQ because the five APIs I’d created needed to talk to each other. I then spent ages wrestling with RabbitMQ on Kubernetes. I’d engineered a problem so I could engineer a solution to it.
I learnt to start lean, add complexity when there’s a real need, and design so that useful additions remain possible later.
Don’t build for imaginary scale at the expense of delivering something useful now.
9. DRY is advice, not a blood oath.
Section titled “9. DRY is advice, not a blood oath.”I’m all for reducing duplication. But sometimes the quest to avoid repeating ourselves makes straightforward code unnecessarily complicated.
I’ve seen a function that needs three enormous objects and two injected services just to perform one operation. Or this absolute triumph of abstraction:
string GetName() => Names.CAT;Why? Is the cat expecting a rebrand?
Then there are the single-use functions extracted because they might be reused someday. Before long, understanding a small bit of logic means opening fifteen methods in fifteen places.
We’re not in the 90s writing QBasic anymore. IDEs have refactoring tools. If something genuinely needs sharing later, we can extract it then, with a much better idea of what should be shared.
Don’t abstract because something might be reused. Abstract because there’s a reason to.
10. Don’t make me dissect your code.
Section titled “10. Don’t make me dissect your code.”“What the hell am I looking at?” — Spaceballs (1987)
There’s a special place in hell for massively nested LINQ expressions and 5,000-character one-liners that somebody thought were terribly clever.
Clever code is rarely as clever as its author thinks. Sometimes it’s more expensive to execute, and quite often it’s an absolute pain in the arse to maintain.
Compilers are clever. You don’t have to sacrifice readability to save a few characters. Calling something item instead of i, or splitting a complicated expression into meaningful steps, isn’t going to bring your application to its knees. And with LINQ, the important performance decisions are about what gets executed and where, not how impressive your one-liner looks.
Then there’s debugging. If something breaks, I want to set a breakpoint, inspect a value and step through the logic. I don’t want a forensic investigation into one enormous expression just to find which part failed.
Your code isn’t a cryptic crossword. Stop treating it like one.
11. A string is not a state machine.
Section titled “11. A string is not a state machine.”I don’t pretend we can remove every runtime check, but if the compiler can catch a mistake before an application starts, why wouldn’t we let it?
One of the things I enjoy about Rust is pattern matching, especially when the compiler can tell you that you’ve missed a case. Combined with sensible defaults and clear code, a decent type system can eliminate quite a lot of unnecessary defensive work.
What particularly bothers me is using arbitrary strings for values with a known set of possibilities. If something has five valid states, use an enum or another structured type. A string can be absolutely anything, including "Compeleted".
I’m not precious about it. I won’t build a complicated type hierarchy to avoid one if statement.
Make invalid states harder to create, but don’t make valid code harder to write.
12. Make it good. Make it better. Make it lean.
Section titled “12. Make it good. Make it better. Make it lean.”“I feel the need… the need for speed!” — Top Gun (1986)
Get it working first, then improve it. I’m not arguing for spending three days shaving nanoseconds off a function that gets called twice a year. Just apply a little common sense. Don’t casually run SELECT * against an enormous production database, for example. Yes, I’ve done it. No, I don’t recommend it.
Look at where work happens. Could a small stored procedure do the job instead of dragging thousands of records into application memory? Do you need another allocation? C# and TypeScript have garbage collectors, but that doesn’t mean we should give them a full-time job.
Faster computers and cheaper resources can make us lazy. A £10 VPS sounds cheap, but if better code fits comfortably on a £5 VPS, that’s a saving. Multiply that across 100 instances and it starts looking rather more interesting.
Users notice, too. We’ve got gigabit broadband and somehow still end up downloading piles of scripts, fonts and frameworks to render something GeoCities managed over dial-up.
Fast, efficient software isn’t a luxury. It’s good engineering.
13. The cloud is just somebody else’s computer. And invoice.
Section titled “13. The cloud is just somebody else’s computer. And invoice.”A company I once worked for, which shall remain nameless, managed to spend more than $50,000 on AWS in one December because somebody forgot to scale down an application over Christmas. With some optimisation, I reckon that workload could’ve run on a single office machine, even at peak demand.
Cloud infrastructure can be fantastic. But it doesn’t make operational problems vanish; often it moves them to another team and sends you an invoice for the privilege.
For a greenfield experiment, consider self-hosting if you have the means. Buy a £20 machine from eBay, put Ubuntu on it, and get something working before you decide you need three cloud certifications to deploy “Hello World”.
Of course, security, backups, uptime and maintenance matter. A bargain-bin PC isn’t the answer to every production problem. But if the only users are six internal stakeholders, start by asking whether the expensive infrastructure is actually justified.
And if your little project takes off and genuinely needs cloud scale, brilliant. Keep the £20 computer as a memento.
Scale because you need to, not because your architecture diagram looks lonely.
14. Treat me like an adult. I can read the code.
Section titled “14. Treat me like an adult. I can read the code.”I’ll give Uncle Bob some credit here: I like self-documenting code. Meaningful names, good structure and sensible functions should make most of the behaviour fairly clear.
What I don’t need is a comment explaining that counter++ increments a counter. Cheers. I’d never have worked that one out.
What I do appreciate is context. Why does that seemingly redundant check exist? Why are we avoiding the obvious solution? Which strange business rule explains this slightly questionable piece of logic?
Code can show me what it does. It can’t always tell me why somebody decided to do it that way.
Document the reasoning, not the obvious.
15. Don’t sweep errors under the carpet.
Section titled “15. Don’t sweep errors under the carpet.”I once had to investigate two devices running a supplier’s software that kept crashing while trying to communicate. Our code looked fine. The supplier naturally insisted it was our fault.
Eventually I went back to basics and fired up Wireshark. Both devices had exactly the same MAC address. No wonder the network couldn’t reliably tell them apart.
After I raised it, the supplier discovered other customers had reported the same problem. Their software caught and logged the errors they expected, but the unexpected ones disappeared into the ether. Nobody had the information needed to diagnose it.
Every error deserves appropriate handling. Recover when you can. When you can’t, preserve enough detail to understand what happened. Don’t catch an exception just to return null, or wrap a useful exception in a generic “Something went wrong” and throw away the context.
Custom exception types can be useful. So can rethrowing an existing exception without losing its original stack. But your users generally need a useful explanation of the outcome, not a raw stack trace served up as a feature.
An error you can’t diagnose is a problem you’ve made harder to solve.
16. If it ain’t broke, don’t rewrite it because you’re bored.
Section titled “16. If it ain’t broke, don’t rewrite it because you’re bored.”I’ll admit it: I have a bit of a red flag. I love tearing things apart and rebuilding them. My “measure thrice, cut once” has occasionally become “measure thrice, cut once, then remake the entire thing because I reckon I can do it better”.
I’ve learnt to control myself. Mostly.
A shiny frontend framework that’s two bytes smaller than the existing one isn’t a business case for rewriting your application. If it works and isn’t causing trouble, leave the poor thing alone.
Rewrites can be justified: unsupported technology, serious security problems, or a quick-to-build MVP that has outgrown its original implementation. A solid test suite helps make sure you improve the guts without accidentally losing the behaviour.
And don’t confuse consistency with loyalty. At Country Eats, C# worked well for our business APIs, while some slower Node.js processing services were better suited to Go. We replaced those pieces rather than tearing down everything.
If you’re building a new service, it doesn’t have to use the same language as the rest of the system just to match. Use C#, Go, Rust, or, if you can somehow justify it, VBA.
Don’t rewrite for novelty. Don’t preserve for loyalty. Choose what makes sense.
17. Write code for future you. He’s a judgemental bastard.
Section titled “17. Write code for future you. He’s a judgemental bastard.”I don’t particularly write code for the next developer. I write it for future me, and he’s a difficult person to please.
I’ve looked back at old code and thought, “Christ, I must’ve been having a bad day.” I’ve even left myself comments like // Roy, don't do it that way. It doesn't work. That's why it's like this. Sometimes future me needs reminding that past me had a reason.
I once returned to a company I’d left two years earlier to improve code I’d originally written. I looked at it, turned to the manager, and said, “I’m so sorry for this shit. Did I have coffee that day?” We laughed. I rewrote it.
I keep that in mind now. I want to be able to revisit a project months or years later and understand what I did and why, even if I’ve since learnt a better way.
Other people don’t have to love my choices. There are plenty of legitimate approaches to the same problem.
I don’t mind if other people don’t like my code. I mind if I don’t like my code.
18. I’d rather fail at building something than fail to build something.
Section titled “18. I’d rather fail at building something than fail to build something.”“Do. Or do not. There is no try.” — The Empire Strikes Back (1980)
I worked at a greenfield company where we spent nearly six months trying to design the perfect architecture. Meeting after meeting, diagrams upon diagrams, debating every conceivable possibility. Meanwhile, the company was coming dangerously close to running out of money.
Eventually, in what felt like the ten-thousandth meeting, I said, “Look, let’s build something. If we screw up, we screw up. At least we’ll learn. I’d rather we fail at building something than fail to build something.”
I’ve said much the same thing when two developers disagree about an approach. Why not try both? Build a small example and compare. Nine times out of ten we’ll probably land somewhere in the middle anyway.
Planning matters. But there’s a point where planning becomes procrastination with diagrams.
You can’t learn from software you never build.
19. Automation should save effort, not become a second job.
Section titled “19. Automation should save effort, not become a second job.”The engineer in me wants to automate absolutely everything. Why do something manually when I could spend a week writing software to do it for me?
That’s burnt my arse more than a few times.
I once spent a week building a glorious script to scaffold API controllers: names, templates, service injection, registration, tests, the lot. I was practically giggling at the thought of the whole team creating standardised endpoints in seconds.
I demonstrated it once. We didn’t need another controller for ages. By the time we did, the architecture had changed, the script broke, and my masterpiece was quietly removed from source control.
On the other hand, I’ve built pipeline automation that was still being used years after I left. Automation isn’t the problem. Judging whether it will repay its cost is the trick.
AI adds another dimension. Rather than asking it every week to export records, organise CSV files and email a report, perhaps ask it to help you create a small, reusable script you can inspect and run yourself. Less repetitive prompting, more repeatability.
Sometimes consistency and fewer mistakes justify automation even when the time saving is modest. Sometimes the input form for your automation is more work than doing the job. That’s usually a clue.
Automate the repetition, not the opportunity to create more work.
20. Choose tools for the problem, not the hype.
Section titled “20. Choose tools for the problem, not the hype.”I love a shiny new language or framework as much as the next developer. The question is whether you’re choosing it because it helps the business, or because everybody’s talking about it.
I’ve done both.
When we were still using ASP.NET WebForms, moving to ASP.NET MVC was a genuine improvement. Better tooling and a faster, more pleasant way of building things.
When TypeScript first appeared, I got caught up in the excitement and introduced it at work before the ecosystem was really ready for what we needed. We ended up wrestling with Gulp, RequireJS and mountains of configuration to produce JavaScript we could’ve written directly. Lesson learnt.
At another company, a developer showed me a sluggish real-time map fed by server-sent events. It was buried under Next.js, React, reactive state and constant map redraws. In about twenty minutes of pair programming, we demonstrated that plain HTML, JavaScript and the same map library could do the job perfectly well.
And then there was the junior developer who spent ages getting Docker set up to host a static website.
Mate. It’s a static website. Just host it.
I’m not against frameworks, containers or new languages. I just want them to make the problem easier, rather than become another problem to solve.
Use the tools the job needs, not the ones you fancy putting on your CV.
21. Learn the language, not just the framework.
Section titled “21. Learn the language, not just the framework.”“There is no spoon.” — The Matrix (1999)
I think too many developers learn a framework without understanding much of the language or platform underneath it. I’m not saying everybody needs to recite the seven-layer OSI model backwards, but fundamentals matter.
In one of my earliest interviews, I announced that I loved jQuery but hated JavaScript. I mostly meant JavaScript’s syntax, but hadn’t quite appreciated the minor detail that jQuery is JavaScript. Not my finest moment.
Since then, I’ve favoured learning languages before frameworks. I learnt TypeScript before Angular. Understand vanilla JavaScript and React, Vue, Angular or whatever comes next become easier to pick up. The framework-specific details are in the docs.
Which makes some recruitment conversations rather entertaining.
“Have you got five years of professional React experience?”
“No, but I’ve got more than fifteen years of JavaScript experience.”
“Ah, that’s a shame.”
Or: “We need someone with ten years of Entity Framework experience.”
You mean the ORM built around C#, LINQ and database concepts? Those things?
Frameworks come and go. Fundamentals travel with you.
Learn to drive the car, not just operate the satnav.
22. AI is my power drill, not my architect.
Section titled “22. AI is my power drill, not my architect.”“With great power comes great responsibility.” — Spider-Man (2002)
I’m what I like to call a BT developer. Before Tokens.
I remember being thirty Stack Overflow tabs deep, trawling through documentation, and even reading books. Actual books. Paper things with words printed on them. Dark times.
These days I use AI to write a considerable amount of my code, and I make no apologies for it. AI is here to stay and I welcome it. But there’s a difference between using AI to write code and letting it engineer your software.
I treat it like an incredibly gifted junior intern. It can do remarkable work at extraordinary speed, but it needs direction and review from someone who understands the problem. I design the architecture, make the decisions, inspect the output, and change what I don’t like. I don’t just vibe something into existence and hope for the best. If I approve and deploy the result, it’s my responsibility.
For people learning programming, AI can be brilliant. It can explain the same concept seventeen different ways until one finally clicks. That’s a considerable improvement over asking an innocent question on Stack Overflow, getting downvoted into oblivion, being pointed to a vaguely related answer from 2009, and being told by someone with 400,000 reputation points that you shouldn’t be programming at all.
Nobody should be made to feel stupid for trying to learn.
But there’s a difference between learning with AI and outsourcing your understanding to it. I wouldn’t hire somebody who couldn’t explain an if statement, however impressive their AI-generated portfolio looked.
I’m not particularly worried about AI replacing my job. It’s another tool, like a project scaffolder, an IDE or a power drill replacing a manual screwdriver. I still need to know where the screw goes, the angle, and how deep to drive it. The drill just saves my wrist.
AI can write my code. It doesn’t get to make my engineering decisions.
In the end
Section titled “In the end”Build things. Question everything, including this manifesto. Prototype before committing. Respect the user’s time, the machine’s resources, and your future self’s patience.
Don’t worship tools, frameworks, principles or personalities. Learn from mistakes, especially your own. Write as little code as you need, but make it count.
And if you ever catch me deploying six microservices to serve “Hello World”, please take my keyboard away.
Support the project: Buy Me a Coffee or GitHub Sponsors.