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:
- Can we run it locally?
- Can we restore it?
- Can we replace it?
- Can we debug it at 2am?
- Can we remove it without rewriting the product?
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:
- One oversized server
- Postgres for primary data
- Files on local disk
- Containerised app deployment
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:
- Regular
pg_dumpbackups - Incremental backups to object storage with
restic
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.
- Normal relational tables for core data
- Materialised views for caching
- Unlogged tables for fast transient data
- A Postgres-backed queue library such as
pg-boss - Scheduled jobs with
cronorpg_cron - Built-in full-text search is more than enough for most apps
- Append-only tables for audit trails and logs.
- Time-series data with partitioning and rollups.
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:
- Different services need different runtimes
- A critical ingestion path must stay isolated
- Parts of the system scale differently
- Different teams need independent ownership
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.