No wall of definitions. We take MiniChat from 100 users to 50 million and let each new problem force the next idea. Every section starts with the problem that appears, then a real-world picture, then plain words, then a diagram.
Always walk the same six steps, out loud. Clarify what we're building, estimate how big, sketch the API, draw the boxes, then zoom into one hard part, then hunt for what breaks. The order matters: each step feeds the next.
Now the words: steps 1-2 define functional and non-functional requirements. Step 4 is your high-level architecture. Step 6 is finding the single point of failure and the bottleneck.
Estimate three things: traffic per second, how much you store, and how much data moves over the wire. Round hard. The goal is the right power of ten, not a spreadsheet.
| Quantity | Rough math | Answer |
|---|---|---|
| Daily actives | 50M total × 20% active | 10M DAU |
| Messages/day | 10M × 40 msgs | 400M/day |
| Write QPS | 400M / 86,400s | ~4,600/s |
| Peak QPS | ×3 for busy hours | ~14,000/s |
| Storage/day | 400M × 300 bytes | ~120 GB/day |
| Storage/year | 120 GB × 365 | ~44 TB/yr |
Split requirements into two piles. Functional = the features (send, receive, history, presence). Non-functional = the qualities (fast, always up, durable, scales). Then promise a number for the qualities so everyone agrees what "good" means.
| Term | Plain meaning | MiniChat example |
|---|---|---|
| SLI | The thing you measure | % of requests under 200ms |
| SLO | Your internal target | 99.9% under 200ms |
| SLA | The promise with penalties | 99.9% uptime or we refund |
Now the words: SLI is the indicator (measured), SLO is the objective (target), SLA is the agreement (contractual, with consequences). SLI ⊂ SLO ⊂ SLA.
Two ways to grow. Vertical = make the one machine bigger (more CPU/RAM). Simple, but there's a ceiling and it's a single point of failure. Horizontal = add more machines and split the work. Nearly unlimited, survives failures, but only works if each machine is stateless (keeps no session in local memory, so any machine can serve any request).
Now the words: vertical = scale up, horizontal = scale out. A machine holding no local session is a stateless service. The shared store is where the state lives.
Put a load balancer in front. Every request hits it first, and it forwards to a healthy backend. It constantly health-checks the servers and quietly stops sending traffic to dead ones. It's also your public front door, so clients never see individual servers.
| L4 (transport) | L7 (application) | |
|---|---|---|
| Sees | IP + port only | the full HTTP request (path, headers) |
| Speed | very fast, cheap | slower, does more work |
| Can do | raw connection routing | route by URL, sticky sessions, end TLS |
Now the words: spreading requests uses an algorithm: round-robin (take turns), least-connections (fewest busy), or IP-hash (same client → same server). L4 routes on IP/port; L7 reads the HTTP request and routes on content.
A cache is a small, fast store of copies of hot data. Check it first; only touch the slow database on a miss. Caches live at every layer: the browser, a CDN near the user, a shared server-side cache like Redis, and inside the DB itself.
| Write strategy | How it works | Trade-off |
|---|---|---|
| Cache-aside | app reads cache, on miss loads DB & fills cache | simple, most common; first read is slow |
| Write-through | write to cache AND DB together | always fresh; writes slower |
| Write-back | write to cache, flush to DB later | fast writes; data loss risk if cache dies |
Now the words: found in cache = hit, not found = miss. Kicking out old entries = eviction. Time-to-live = TTL. Least-recently-used = LRU.
Pick storage by shape. SQL (Postgres, MySQL) = rigid tables, joins, transactions, strong guarantees. Great for users, payments, anything relational. NoSQL (Cassandra, DynamoDB, Mongo) = flexible, spreads across many machines easily, huge write volume. Great for the message firehose. Either way, add an index on the columns you search by so lookups are instant instead of full scans.
| SQL | NoSQL | |
|---|---|---|
| Shape | fixed schema, tables + joins | flexible: key-value, document, wide-column |
| Scale | scales up; sharding is manual work | scales out horizontally by design |
| Guarantees | strong (ACID transactions) | often eventual; tuned for throughput |
| Use for | users, orders, money | messages, logs, feeds, sessions |
Now the words: ACID = Atomic, Consistent, Isolated, Durable (SQL's promise). NoSQL families: key-value, document, wide-column, graph. An index is usually a B-tree or hash structure.
Replication makes copies: one leader takes writes, several followers copy it and serve reads. Spreads read load and gives you a spare if the leader dies. Sharding splits the data itself across machines, each shard holds a slice, so total size and write load scale out.
| Shard by | How | Watch out |
|---|---|---|
| Hash | hash(key) % N picks the shard | even spread; adding shards reshuffles everything |
| Range | A-M here, N-Z there | simple range scans; can create hot shards |
Now the words: leader-follower is also called primary-replica. Copying is replication (sync = wait for followers, async = don't). Splitting = sharding / partitioning. The column you split on = shard key / partition key.
When the network splits (a partition, the P), a distributed system can keep only one of two things: every node agrees on the same latest data (Consistency), or every request still gets an answer (Availability). You must choose CP or AP for that moment. You can't skip P; networks do fail.
Now the words: CAP = Consistency, Availability, Partition-tolerance. During a partition you get CP or AP. PACELC adds the normal-time trade: Else Latency vs Consistency.
Strong = every read sees the latest write, always. Safe, but slower and less available. Eventual = reads may lag briefly, then catch up. Faster and more available. The useful middle: read-your-writes, which guarantees you always see your own changes even if others lag.
| Model | Guarantee | Use for |
|---|---|---|
| Strong | read always sees latest write | balances, inventory, auth |
| Eventual | catches up after a short delay | like counts, view counts, feeds |
| Read-your-writes | you always see your own writes | your own sent messages, profile edits |
Put a queue between the fast part (accept the message, reply "got it") and the slow parts (LLM, push, moderation). The producer drops a message on the queue; consumers pull and process at their own pace. Spikes just make the queue longer instead of losing work. Many consumers can subscribe (pub/sub).
Now the words: Kafka (log, replayable, high throughput), SQS/RabbitMQ (task queues). Sender = producer, reader = consumer, broadcast-to-many = pub/sub. Same input, same result = idempotent.
Pick a style. REST = simple URLs and verbs over HTTP, universal, easy to cache. gRPC = fast binary, strict contracts, great between your own services. GraphQL = client asks for exactly the fields it wants in one call, great for varied clients. Then handle the basics: page large lists, version so you can evolve, and use idempotency keys so a retried "send" doesn't duplicate.
| REST | gRPC | GraphQL | |
|---|---|---|---|
| Best for | public APIs, browsers | service-to-service | many client shapes |
| Format | JSON over HTTP | binary (protobuf) | JSON, custom query |
| Weakness | over/under-fetch | not browser-native | caching + complexity |
/v1/ in the path or a header, never break an old client silently.Now the words: an idempotency key is a unique id the client sends with a write so the server dedupes retries. REST resources use HTTP verbs (GET/POST/PUT/DELETE). gRPC uses protobuf over HTTP/2.
Count each caller's requests and reject once they exceed a limit. Token bucket allows bursts up to the bucket size, then throttles to the refill rate, this is what most APIs use. Leaky bucket instead drains at a fixed, smooth rate no matter the input, good when the downstream needs a steady stream.
| Token bucket | Leaky bucket | |
|---|---|---|
| Bursts | allowed (up to bucket size) | smoothed out, never bursty |
| Output | variable, up to a cap | constant, fixed rate |
| Feels like | most public APIs | steady drip to a fragile backend |
Now the words: rejected requests get HTTP 429 Too Many Requests. Limits are keyed per user / IP / API key. In a fleet, the counter lives in a shared store (Redis) so all servers agree.
hash(key) % N. It worked, until we added one server. Now N changed, and almost every key maps to a different server. The whole cache misses at once and the DB gets crushed.Map both servers and keys onto a ring (a circle of hash values). A key belongs to the first server clockwise from it. When a server joins or leaves, only the keys in its immediate arc move, not the whole cache. Add virtual nodes (each server sits at many points) so the load spreads evenly.
Now the words: the circle is the hash ring. Extra placements per server are virtual nodes (vnodes), they smooth out uneven load.
Build in defenses. Retries handle blips, but use exponential backoff + jitter (wait longer each time, with randomness) so you don't stampede. A circuit breaker stops calling a service that's clearly down and fails fast. Bulkheads isolate resources so one drowning feature can't sink the rest. Always set timeouts, never wait forever.
Now the words: redundancy = spare copies; failover = switch to the spare automatically. Bulkhead = separate thread/connection pools per dependency. Circuit breaker states: closed → open → half-open.
Instrument three things. Logs = timestamped text of individual events. Metrics = numbers over time (QPS, latency, error rate) you graph and alert on. Traces = follow one request across every service it touches, so you see exactly which hop was slow.
| Method | Watch | Good for |
|---|---|---|
| RED | Rate, Errors, Duration | request-driven services (APIs) |
| USE | Utilization, Saturation, Errors | resources (CPU, disk, queues) |
Now the words: the three together are the three pillars of observability. A trace is made of spans (one per hop). p99 = the latency 99% of requests beat.
A monolith is one deployable app: simple, fast to build, great early. Microservices split it into independent services (auth, chat, search, billing), each owned by a team, deployed and scaled alone. The catch: a business action now spans services, so you can't use one database transaction.
| Monolith | Microservices | |
|---|---|---|
| Deploy | all at once | independently, per service |
| Scale | whole app together | just the hot service |
| Complexity | low (start here) | high (network, ops, data) |
| Data | one shared DB | database-per-service |
Now the words: start with a modular monolith; split only when team or scale pain is real. Cross-service consistency uses sagas and compensating transactions, not distributed 2-phase commit (fragile).
Two distinct steps. Authentication (authn) proves identity, usually a login that returns a token. Authorization (authz) checks permissions on every request. Encrypt data in transit with TLS and at rest on disk. Keep passwords hashed and secrets out of code. Layer defenses so one failure isn't fatal.
Now the words: JWT = a signed token the client carries proving who they are. OAuth2 = the delegated "log in with Google" flow. TLS = the S in HTTPS. authn ≠ authz: identity vs permission.
1. Clarify: shorten a URL, redirect fast, optional custom alias + analytics. Read-heavy (people click far more than they create).
2. Estimate: 100M new URLs/month ≈ 40 writes/s. Read:write ~100:1 → ~4,000 reads/s. 100M × 500 bytes × 12 months ≈ 0.6 TB/yr. Small data, huge read traffic → cache hard.
3. API: POST /urls {long_url} → {short}. GET /{short} → 301 redirect. Idempotency key on create.
4. Encoding: take a unique 64-bit id from a counter/Snowflake, base62 encode it (a-z A-Z 0-9) → a 7-char code covers 62⁷ ≈ 3.5 trillion. Avoids the collision checks a random hash needs.
5. Schema: short_code (PK) ยท long_url ยท created_at ยท owner. A key-value store is ideal: lookup by code is a single get.
6. Scale & bottlenecks: shard the KV store by short_code (consistent hashing). Cache hot codes in Redis + CDN so 90%+ of redirects never hit the DB. Redirects are stateless, so scale app servers horizontally behind the LB.
1. Two pipelines. Ingestion (offline): documents → chunk → embed → store vectors. Query (online): user question → embed → retrieve similar chunks → stuff into the prompt → LLM → stream answer. RAG = give the model the right context so it answers from facts, not memory.
2. Vector store & retrieval: store embeddings in a vector DB (Pinecone, pgvector, Milvus). At query time, embed the question and fetch the top-k nearest chunks (approximate nearest neighbor, ANN). Keep each tenant in its own namespace so no one retrieves another company's docs.
3. LLM gateway: a single choke point in front of all model calls. It handles model routing (cheap model for easy questions, strong for hard), fallback if a provider is down, retries with backoff, token/cost accounting, and streaming responses back to the user token by token.
| Concern | Technique |
|---|---|
| Latency & feel | stream tokens (SSE) so the answer appears live |
| Cost | semantic cache (reuse answers for similar questions), route to cheaper models, cap context size |
| Fairness | rate-limit per tenant so one heavy user can't starve others |
| Isolation | per-tenant vector namespaces + row-level authz on every retrieval |
| Safety | guardrails: strip/validate retrieved text, detect prompt injection, filter output |