The cost of one more box
It's tempting to design like the big companies: a message queue here, a search cluster there, a separate service for every feature. Each piece is real and useful at the right scale. But every component you add brings costs with it:
- Another thing to fail. Parts in a chain multiply their failure chances (01.03 Availability, durability, consistency, latency, cost).
- Another hop. Each network call adds about half a millisecond, plus its own slow tail.
- Another copy of data that can drift out of step with the original.
- Another thing to run: deploy, monitor, upgrade, secure, and understand at 3 a.m. when it breaks.
- Money. Machines, managed services and the people's time to look after them.
The best design is usually the simplest one that meets the requirements, with a clear idea of what you'd add next and when.
Snip, over-built and right-sized
Here's a Snip design that looks impressive, and what's left when every component has to justify itself against the estimate:
ChangeKeep only what a requirement or estimate calls for
Text version
Change: Keep only what a requirement or estimate calls for
Added: Postgres.
Removed: Redis cluster, Kafka, Cassandra, Elasticsearch.
New connections: Snip API → Postgres (reads, writes).
Each removed piece had a cost and no requirement behind it:
- A queue for writes at 10 writes a second adds a hop and delays a link becoming usable, which breaks the "works immediately" requirement.
- A sharded database for 300 GB splits data that fits comfortably on one machine, and makes every query that spans shards harder.
- A search cluster serves a feature (searching links) that isn't in the requirements at all.
- A six-node cache for 1,000 reads a second solves a problem the database doesn't have yet.
What stays is what a requirement asks for: a second API instance and a standby database copy, because redirects must keep working when one machine fails.
When you do need it
Add a component when you can point to one of these:
- A requirement: "redirects must survive a server failure" calls for a second copy.
- An estimate: "35,000 reads a second" is past what one database comfortably serves, so add a cache or replicas.
- A measurement: in a real system, a graph showing the database at 90% CPU at peak beats any guess.
And before reaching for a new system, check what you already run can do the job. Postgres alone can act as a job queue
(SELECT … FOR UPDATE SKIP LOCKED), a full-text search engine and a key-value store well enough for many products.
Using one well-understood tool for three jobs often beats running three tools.
In an interview
Interviewers usually want to see you scale a design, so "one server is enough" can't be your whole answer. Show both halves: say what's enough for the stated numbers, then say what you'd add and at what point. "At this scale one Postgres primary handles the writes; past a few thousand reads a second I'd add a cache in front of it" shows you know both the tool and when it's needed.
Seen in production
Stack Overflow
2016The whole site
On one day in February 2016, Stack Overflow's load balancers handled over 209 million HTTP requests. The sites ran on 4 Microsoft SQL Servers, 11 IIS web servers, 2 Redis servers, 3 tag-engine servers, 3 Elasticsearch servers and 4 HAProxy load balancers, with Redis instances below 2% CPU.
What we foundA very large site ran on a small, carefully tuned set of servers; they scaled up and tuned before they scaled out.
Stack Overflow: The Architecture – 2016 Editionnickcraver.com ↗Snip creates about 10 links a second at peak. A teammate suggests putting a message queue in front of the database for all writes 'so it scales'. What's the best response?
Recap
Interview questions
When would you not add a cache, queue or extra service to a design?