There are no right answers in architecture. There are only positions on a handful of axes, chosen on purpose. Tonight we name all six, so you stop guessing and start deciding out loud.
Junior engineers want the right database. Staff engineers know the database is downstream of a position they took on an axis. Architecture is choosing where to sit on each trade-off, deliberately, and being able to say why.
There's rarely a single "best" choice. There's only "best for what you care about most right now." Picking fast usually means giving up cheap, or simple, or something else, and that's completely fine, as long as you can say what you gave up and why.
CAP is the soundbite; PACELC is the useful form. Read it as one question with two branches, and notice you're paying somewhere on both.
flowchart TB
P{"Network partition
happening?"}
P -- "Yes · the P" --> PICK{"Pick one,
you can't keep both"}
PICK -- Availability --> AP["Cart · feed · presence
serve stale, reconcile later"]
PICK -- Consistency --> CP["Ledger · inventory · auth
refuse rather than be wrong"]
P -- "No · the E (Else)" --> ELSE{"Even healthy,
still trading"}
ELSE -- Latency --> L["answer fast,
guarantee weaker"]
ELSE -- Consistency --> C["guarantee strong,
wait a little longer"]
classDef ap fill:#DCEBFE,stroke:#1F2937,color:#0E1726;
classDef cp fill:#FEF3C7,stroke:#1F2937,color:#0E1726;
classDef ask fill:#ffffff,stroke:#1F2937,color:#0E1726;
class AP,L ap;
class CP,C cp;
class P,PICK,ELSE ask;
The staff tell: name the user-visible effect on a leaf, "they see a stale count for ~2s," not "eventually consistent."
Every batch, queue, and buffer raises requests-per-second and adds delay to the request that waits for the batch. They are not the same number and they do not optimize together.
| Latency | time for one request · graded at p99, not the mean |
| Throughput | requests served per second across the whole system |
| The lever | batch / buffer / async → throughput up, tail latency up |
| The tell | state the budget: "p99 < 50 ms" and which one you're protecting before you reach for a queue |
The classic case is the feed. Same post, two ways to spend the work, and the bill just moves between the write path and the read path.
flowchart LR POST(["New post"]) POST --> FW["Fan-out on WRITE
copy into every follower timeline now"] POST --> FR["Store once
normalized, do nothing extra"] FW --> FWR(["Read = 1 cheap lookup
but a celebrity's write fans out to millions"]) FR --> FRR(["Read = join across everyone you follow
cheap writes, slow reads"]) FWR --> HY{"Hybrid:
push for most,
pull for celebrities"} FRR --> HY classDef w fill:#D6F5E3,stroke:#1F2937,color:#0E1726; classDef r fill:#FEE4E2,stroke:#1F2937,color:#0E1726; classDef h fill:#FEF3C7,stroke:#1F2937,color:#0E1726; class FW,FWR w; class FR,FRR r; class HY h;
The tell: find the hot path, optimize it, and say out loud what you're paying on the cold one (here, write amplification and drift you must reconcile).
Caches, indexes, materialized views, denormalized copies, bloom filters: all the same move. You hold extra bytes so you can skip computation. The hidden cost is rarely the bytes, it's the staleness and invalidation.
A monolith is cheap to build, deploy, and reason about, and it couples everything's release and scaling together. Services buy independent deploy and scale, and charge you network calls, partial failure, and distributed data. Splitting for the org chart, not a real axis, is how you get a distributed monolith.
Performance, reliability, and time-to-market are all bought with money or time. Active-active multi-region roughly doubles infra for one more 9. Build-vs-buy is this axis. The staff move is tying the spend to a business number, not "best practice."
| 99.9% → 99.99% | ~8.8h → ~52m downtime/yr · often 2× infra for the redundancy |
| Build vs buy | buy = $/mo + lock-in · build = headcount + maintenance forever |
| The tell | "this 9 is worth it because an hour down costs us $X" beats "we should be highly available" |
One sentence, three axes moved at once. Staff engineers don't say "add a cache," they account for all three out loud.
A "cache" is just a small fast copy of data you keep close so you don't have to fetch the real thing every time, like jotting a phone number on a sticky note instead of looking it up in the contacts app each call. The catch: when the real number changes, your sticky note is now wrong.
You optimized the read path and added write-side work to keep the cache warm and correct.
You spent RAM to skip computation, and accepted a staleness window you should name.
A cache that can serve stale on a miss-storm is an availability/consistency choice too.
Cheap tactics are rarely free, they relocate the cost to another axis. Track where it went.
Four sentences. Run them on any decision and you sound like you chose it, because you did.
Bring a choice from your current system: a cache, a queue, a split, a database pick. We'll do one together, then you run the four sentences on yours and drop the axis + position in the chat.
Consistency↔Availability · Latency↔Throughput · Read↔Write · Space↔Time · Coupling↔Flexibility · Cost↔Everything. For every call: name the axis, take a position, own the consequence, set the trigger.