LESSON 1.3
Everything is a trade-off
Every design choice gives something up. Learn the trade-offs that come up in almost every system, and how to pick one deliberately.
7 min read
There is no free lunch in system design. Every component you add, every guarantee you make and every shortcut you take moves the system along some axis, and away from another. Good designers do not avoid trade-offs; they choose them on purpose and say why.
The trade-offs you will meet everywhere
These pairs come up in almost every design in this course. Learn to recognize them on sight.
Latency vs consistency
Copies of data make reads fast: a cache next to the server, a replica in the user's region. But the moment there are copies, they can disagree. You either wait for all copies to agree before answering (slower, consistent) or answer from the nearest copy (fast, maybe stale).
Read from the primary database
- Always the latest value
- Slower
- One machine takes every read
Pick it when: The value must be exact: balances, seat reservations, inventory at checkout.
Read from a cache or replica
- Fast
- Spreads load
- May be seconds out of date
Pick it when: Slight staleness is invisible or harmless: feeds, profiles, product pages.
Simplicity vs scalability
One server and one database is easy to build, debug and reason about. A fleet of services with queues, caches and shards can handle enormous load, but it is harder to build, harder to operate and has many more ways to fail. Start simple and add complexity only when a requirement demands it.
Synchronous vs asynchronous
Doing work during the request means the user gets the result immediately, and also waits for it. Pushing work to a queue gives a fast response and lets the work retry, but the result arrives later and you now need to handle duplicates and ordering.
Cost vs everything
More replicas, more regions and more cache memory all cost money. Five nines of availability (about 5 minutes of downtime a year) can cost many times what three nines (about 9 hours) costs. The right level depends on what downtime costs the business.
Read-optimized vs write-optimized
Indexes, denormalized copies and precomputed results make reads fast because the work was done at write time. That makes writes slower and storage bigger. A read-heavy system (most consumer apps) usually accepts this happily. A write-heavy system (logs, metrics) usually does not.
How to choose
When you face a choice, walk through the same four questions:
- What are the options? Name at least two. If you can only think of one, you have not chosen anything yet.
- What does each give, and what does each cost? Latency, consistency, cost, complexity, failure modes.
- Which requirement decides it? "Feeds can be a few seconds stale" or "never double-charge".
- What would make you change your mind? "If writes grew 10x, I'd move counters out of Postgres."
A worked example
You are storing a "like" count on posts. Options:
- Update a counter column in the posts table on every like.
- Insert a row per like and count them when displaying.
- Accumulate likes in memory or a cache and flush a total every few seconds.
Option 1 is simple and exact, but a viral post means thousands of writes per second to one row, a hot spot. Option 2 makes writes cheap and exact but turns every page view into a count over millions of rows. Option 3 handles huge write rates and cheap reads, but the number shown can lag by a few seconds, and you can lose a few seconds of likes if a node crashes.
For a social app, where nobody minds a like count that is a few seconds behind, option 3 (or 1 with batching) is usually right. For votes in an election, it would be unacceptable. Same feature, different requirements, different answer.