There's a question you'll ask yourself more than any other in system design: "Which option is best?" The honest answer is almost always: "Best for what?"
Every design choice gives something up. A cache makes reads fast and introduces stale data. A second server adds capacity and makes deploys harder. A queue keeps checkout fast and makes failures harder to see. There is no option that only has upsides. If you think you've found one, you haven't found its downside yet.
What a trade-off looks like
A trade-off is a choice where getting more of one thing means getting less of another.
Take a simple Tiffin decision: when a restaurant updates a dish's price, how quickly must customers see the new price?
| Option | What you gain | What you give up |
|---|---|---|
| Read the price from the database on every request | Always correct | Slower menus, more load on the database |
| Cache menus for 5 minutes | Fast menus, light database | A customer may see an old price for up to 5 minutes |
| Cache, and clear the cache on every price change | Fast and mostly fresh | More code, and new bugs when clearing fails |
None of these is "correct". Which one is right depends on the requirement. If a stale price just means Tiffin charges the old price for a few minutes, the 5-minute cache is fine. If it could mean charging more than the customer saw, you need the third option, or a check at checkout.
The trade-offs you'll meet again and again
Most design decisions are a version of one of these:
- Speed vs freshness. Caches, replicas and CDNs make reads fast by serving copies. Copies can be out of date. (Modules 3 and 5.)
- Consistency vs availability. When machines can't talk to each other, you either refuse some requests or risk serving different answers. (Module 13.)
- Latency vs throughput. Batching work gets more done per second but makes each item wait longer. (Modules 7 and 17.)
- Simplicity vs scale. One database is easy to reason about. Sixteen shards scale further but make every query, migration and incident harder. (Modules 9 and 14.)
- Cost vs everything. More servers, more regions, more replicas all cost money. Every nine of uptime costs more than the last. (Module 18.)
- Build now vs build later. Building for scale you don't have yet costs time today. Not building it may cost a rewrite later. Usually, later is cheaper than you think.
How to choose
1. Go back to the requirements
A trade-off is only hard when you don't know what matters. The requirements tell you. "Never charge twice" means you trade some speed for correctness in payments. "Menus must be fast at dinner" means you trade some freshness for speed in menus.
Notice that the same system makes opposite trade-offs in different places. That's normal and good. Payments get strong correctness. Menus get speed. Rider location gets "good enough and recent".
2. Ask what it costs to be wrong
If the cache shows a dish that just sold out, the customer gets a "sorry, unavailable" at checkout. Mildly annoying. If the payment system charges twice, the customer files a complaint, support issues a refund, and you might lose that customer. Spend your complexity where mistakes are expensive.
3. Prefer the choice that's easy to undo
Some decisions are cheap to reverse: adding a cache, changing a timeout, adding a server. Others are expensive: your database choice, your data model, splitting into microservices. Make reversible decisions quickly. Slow down for the irreversible ones.
4. Say the trade-off out loud
When you explain a design, don't just say what you chose. Say what you gave up and why that was acceptable:
"We cache menus for 60 seconds. That means a price change can take up to a minute to show. That's acceptable because checkout re-reads the price from the database, so nobody is ever charged a price they didn't see."
That one paragraph tells the listener you understood the downside, sized it, and protected the part that matters. It's the single most useful habit in system design, in interviews and at work.
Name the trade-off
Tiffin's team wants to show "23 people ordered this in the last hour" next to popular dishes. Option A counts orders live from the database on every menu view. Option B updates the numbers once every 10 minutes in a background job. Which would you pick, and what are you giving up?
Show one answer
Option B. You give up freshness: the number can be up to 10 minutes old. That's fine because it's a rough social signal, not something anyone acts on precisely. In return you avoid running a counting query on every single menu view at dinner time, which is exactly when the database is busiest.
If the team later wants it live, they can revisit. It's an easy decision to undo.
Beware the "free" option
Some choices look free because their cost is hidden or delayed:
- "Let's use microservices from day one." The cost shows up later: network calls that fail, data spread across services, and every feature needing three teams.
- "Let's add Kafka, we'll need it eventually." The cost is a new system to run, monitor and understand, for a problem you don't have yet.
- "Let's just retry on failure." The cost appears during an outage, when retries multiply the load on a service that's already struggling.
When an option seems to have no downside, look harder. The cost is usually in operations, in failure modes, or in the future.
What to remember
- Every design choice gives something up. "Best" only means something relative to a requirement.
- The common trade-offs: speed vs freshness, consistency vs availability, latency vs throughput, simplicity vs scale, and cost vs everything.
- Choose using the requirements and the cost of being wrong. Spend complexity where mistakes are expensive.
- The same system can make opposite trade-offs in different places, and should.
- Move fast on decisions that are easy to undo. Slow down on ones that aren't.
- Always say what you gave up and why it's acceptable.