Good engineers make boring choices

Build systems you can run, debug, restore and replace.

Fake technical book cover titled Imaginary Scaling Issues with a serious animal portrait

SaaS apps fail because the stack has become too complex to debug and maintain. A dead server is rarely the reason.

The app cannot be reproduced locally. Bugs require jumping through dozens of files, services and dashboards. Migrations are fragile. Nobody knows where anything lives. The person who built the system left six months ago and now every change feels dangerous.

Start with the smallest system that can work

For every app I work on, I try to keep the stack as lean as possible.

Take this blog, for example. It is just HTML files. Nothing fancy. It works. I can edit anything I want at any time. I have version control with Git and it scales because I have a CDN. I do not need anything else. It is just me working on this alone so why would I need a complex system?

Before I add anything, I ask:

Redundancy is not resilience

Here's an example. I had a client who wanted maximum redundancy. We set up multi-cloud failover so if AWS died, Azure would take over.

In the end, the card on file expired so both providers stopped working and the whole setup fell over.

Hidden coupling is everywhere. Billing, identity, DNS, CI, secrets managers, etc. I can do my best but I won't catch them all.

I have also seen provider lock-in hurt teams hard. A vendor gets acquired, prices jump, an API changes or an account gets suspended. So today, I build in a way I can easily leave.

If the answer to "can we replace it?" is no, that vendor owns a piece of our future.

Optimise for recovery

In my career, hardware has rarely caused the worst incidents. Human error, bad deploys, provider lockouts and architecture flaws did.

I know just can't predict every outage, so I focus on how fast I recover instead.

Systems I can reproduce locally. Deployments I can understand. Dependencies I can replace. I had a lot of incidents where the fix was obvious because I could easily reproduce it on my machine.

What matters when everything's on fire at 2am is whether the failure is understandable and the recovery path is already known.

My default stack for small teams

This is a setup that has worked for me for a long time:

I oversize the machine on purpose. Extra RAM gives room for spikes, leaks and debugging without panic. It also stops premature optimisation work that does not matter yet.

A server with plenty of headroom beats a clever setup with no margin.

My backup plan is deliberately simple:

I can run restore drills on any machine. Rebuild on a fresh server, restore the files and the database, then verify the app works end-to-end.

Postgres gets you surprisingly far

Most of the time, I need a database, a key-value store and a queue. Turns out Postgres already does all of this.

Eventually, I might add Kafka, Redis, Elasticsearch or ClickHouse. But Postgres holds up for a very long time.

Deploy fast

I care a lot about CI speed. If deploys take half an hour, the team only gets a handful of deploys in a day.

I always build one Docker image or binary containing everything: the backend, frontend bundle and any static assets. Cache every build layer aggressively. No separate asset pipeline unless the product actually needs one. The app serves the built assets and the CDN caches them.

This is so important but fast deploys change your behaviour. People ship smaller changes, roll back faster and avoid the giant risky release that happens because pushing to prod is just too painful.

Avoid microservices

I'm not the biggest fan of microservices but they can make sense when:

Most of those things do not apply to most startups and small teams.

Most of the other reasons I've heard come down to organisational issues or shortcomings in the architecture. At this stage, your team is too small for "coordination overhead" or "domain-driven design". And "scaling" is the worst reason of all to use microservices.

A modular monolith is usually the right answer until proven otherwise. Then split it. Keep things simple with HTTP API calls. No Kafka, Pub/Sub or event bus unless the pain justifies it.

They fail in obvious ways and the tooling to monitor and debug them has decades of development behind it.

And if you have to change it later, a monolith can always easily be split later anyway.

Do not mistake platform limits for architecture needs

Most PaaS gives your app so little memory and CPU that you end up dealing with fake scaling issues.

Most of those platforms provision a ridiculously small amount of resources. I have seen teams waste time on caps, cold starts (love serverless?) and workarounds instead of shipping product. If you only have a few gigabytes of RAM to work with, you're going to have to optimise much earlier than you should.

The platform limits shouldn't drive your architecture. I see people adding caches and service splits and queues and whatever they can to work around the fact that they simply just don't have enough CPU or RAM.

Most problems disappears if you start with one big machine. I can get a fully managed server with 32 cores, 256 GB RAM and 2 TB of storage for around €199/m at the time of writing. This thing will absorbe a LOT of traffic.

But what is the server dies? Hardware is rarely what hurts the most. The app goes down because a human messed something up. Even if the server itself dies, recovery is simple enough that the whole app can easily be redeployed somewhere else in a few hours.

Strong engineering is boring by default and innovative by exception. Use the simple thing first. Keep the system easy to run, inspect, restore and replace. Then, when complexity becomes unavoidable, add it deliberately, keep it isolated and make sure you can undo it.