Learn system design by scaling one app ๐Ÿ“ˆ

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.

Our app is MiniChat: an AI chat app. One box, you type, the AI replies. On day one it runs on a single laptop-sized server. By the end it serves a continent. Each section below is the fire we had to put out to get there. Two full worked interviews wrap it up.
Opened 0/20 ยท tap any section to expand
1

Where to even startthe 6-step framework

โ–ถ
๐Ÿค” Problem: "Design MiniChat" is a huge, vague question. If you jump straight to databases you will drown, miss what actually matters, and run out of time. You need a track to run on.
๐ŸŒ Think of it like
A doctor's visit. A good doctor doesn't prescribe before asking questions. They take symptoms, estimate severity, then treat. You interview the problem before you design it.

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.

1 Clarifyrequirements 2 EstimateQPS, storage 3 APIsthe contract 4 High-leveldraw the boxes 5 Deep diveone hard part 6 Break itbottlenecks
Interview tip: spend real time on steps 1 and 2. Candidates who skip clarifying and estimating build the wrong thing beautifully. The interviewer is watching your process more than your answer.

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.

2

How big is big?back-of-envelope math

โ–ถ
๐Ÿค” Problem: someone says "MiniChat has 50M users." Do we need one server or a thousand? One disk or a warehouse? Without numbers you can't pick anything, and you'll either over-build or fall over.
๐ŸŒ Think of it like
Catering a wedding. Before you rent chairs you ask "how many guests, how many meals each?" Rough numbers, done in your head, are enough to order the right amount. Precision comes later.

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.

worked example ยท MiniChat at 50M users
QuantityRough mathAnswer
Daily actives50M total × 20% active10M DAU
Messages/day10M × 40 msgs400M/day
Write QPS400M / 86,400s~4,600/s
Peak QPS×3 for busy hours~14,000/s
Storage/day400M × 300 bytes~120 GB/day
Storage/year120 GB × 365~44 TB/yr
Takeaway: ~14k peak writes/s and tens of TB/yr โ†’ one box cannot do this. We must spread load and shard storage.
Numbers worth memorizing: 1 day ≈ 86,400s (call it 100k). Read/write ratio for a chat app is roughly 10:1 (people read far more than they write). RAM read ~100ns, SSD ~100µs, cross-region network round-trip ~100ms.
3

What it does vs how wellrequirements, SLA / SLO / SLI

โ–ถ
๐Ÿค” Problem: "send a message" is a feature. But "never lose a message" and "reply in under 200ms" are just as important, and they drive completely different design choices. If you only list features, you'll build something correct that's unusable under load.
๐ŸŒ Think of it like
Ordering a car. "Four seats and a trunk" is what it does. "0-60 in 6s, 40 mpg, never breaks down" is how well. The second list is what makes it expensive and hard.

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.

TermPlain meaningMiniChat example
SLIThe thing you measure% of requests under 200ms
SLOYour internal target99.9% under 200ms
SLAThe promise with penalties99.9% uptime or we refund
Availability math: 99.9% ("three nines") = ~8.7 hours down/year. 99.99% = ~52 min/year. Each extra nine costs a lot more money and redundancy. Don't promise nines you can't pay for.

Now the words: SLI is the indicator (measured), SLO is the objective (target), SLA is the agreement (contractual, with consequences). SLI ⊂ SLO ⊂ SLA.

4

MiniChat's one server is meltingvertical vs horizontal, stateless

โ–ถ
๐Ÿค” Problem: traffic tripled. The single MiniChat server hits 100% CPU and requests queue up. We need more capacity, now. Do we buy a bigger machine or add more machines?
๐ŸŒ Think of it like
A coffee shop with a line out the door. Option A: train one super-fast barista (a bigger machine). Eventually one human hits a limit. Option B: hire more baristas (more machines). Ten average baristas beat one superhuman, and if one calls in sick you're fine.

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).

VERTICAL: one bigger box Big Server ceiling + SPOF HORIZONTAL: many boxes app 1 app 2 app 3 shared state(DB / cache)
The unlock: push all session state out of the app servers into a shared store. Now the app tier is stateless and you can add or kill boxes freely. This one move enables load balancing, autoscaling, and rolling deploys.

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.

5

Which server gets the request?load balancers, L4 vs L7

โ–ถ
๐Ÿค” Problem: we now have three MiniChat app servers. But users type one address. Who decides which server handles each request, and what happens when a server dies mid-shift?
๐ŸŒ Think of it like
A restaurant host at the door. Guests don't wander to a random table. The host sees which servers are free, seats you there, and if a section closes, stops sending people to it. That host is the load balancer.

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.

clients Load Balancer+ health checks server 1 ยท healthy server 2 ยท healthy server 3 ยท DOWN (skipped)
L4 (transport)L7 (application)
SeesIP + port onlythe full HTTP request (path, headers)
Speedvery fast, cheapslower, does more work
Can doraw connection routingroute 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.

6

The database is getting hammeredcaching layers & strategies

โ–ถ
๐Ÿค” Problem: every time a user opens MiniChat we re-fetch their profile and recent chats from the database. The same rows, over and over, thousands of times a second. The DB is now the bottleneck, slow and expensive.
๐ŸŒ Think of it like
Snacks on your desk vs walking to the kitchen. The kitchen (database) has everything but the walk is slow. Keep the snacks you reach for most (hot data) in a drawer at your desk (cache). Fast, but small, and it can go stale.

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.

live ยท click a read. Cache = fast, DB = slow
cache empty ยท first read must hit the DB
Write strategyHow it worksTrade-off
Cache-asideapp reads cache, on miss loads DB & fills cachesimple, most common; first read is slow
Write-throughwrite to cache AND DB togetheralways fresh; writes slower
Write-backwrite to cache, flush to DB laterfast writes; data loss risk if cache dies
The hard parts: invalidation (when the DB changes, a stale cache lies) is famously one of the two hard problems in CS. TTL gives each entry an expiry. LRU evicts the least-recently-used when full. Thundering herd: a popular key expires and thousands of requests stampede the DB at once, fix with a short lock or staggered TTLs.

Now the words: found in cache = hit, not found = miss. Kicking out old entries = eviction. Time-to-live = TTL. Least-recently-used = LRU.

7

Where do messages actually live?SQL vs NoSQL, indexing

โ–ถ
๐Ÿค” Problem: MiniChat has users (structured, relational) and a firehose of messages (huge volume, simple shape). One database type won't be great at both. And even finding one user's messages scans millions of rows unless we help it.
๐ŸŒ Think of it like
The index at the back of a book. Without it, finding "load balancing" means reading every page. The index maps the word straight to page 42. A database index is that back-of-book lookup for your rows.

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.

SQLNoSQL
Shapefixed schema, tables + joinsflexible: key-value, document, wide-column
Scalescales up; sharding is manual workscales out horizontally by design
Guaranteesstrong (ACID transactions)often eventual; tuned for throughput
Use forusers, orders, moneymessages, logs, feeds, sessions
Index trade-off: indexes make reads fast but slow down writes (every insert must update the index too) and cost disk. Index what you filter/sort on, not everything.

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.

8

One DB can't hold it allreplication & sharding

โ–ถ
๐Ÿค” Problem: two new pains. (1) All reads hit one database and it's overloaded. (2) 44 TB/year won't fit on one machine's disk anyway. We need copies for read load AND we need to split the data across machines.
๐ŸŒ Think of it like
A library. Replication = photocopy the popular book so many people read at once (copies). Sharding = the collection is too big for one building, so A-M is downtown and N-Z is uptown (split). Different problems, different fixes, often used together.

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.

REPLICATION (copies) Leader (writes) Follower (reads) Follower (reads) SHARDING (split) Shard 1users A-M Shard 2users N-Z Shard 3users 0-9
Shard byHowWatch out
Hashhash(key) % N picks the shardeven spread; adding shards reshuffles everything
RangeA-M here, N-Z theresimple range scans; can create hot shards
Hot partition: if one celebrity user or one date range gets all the traffic, its shard melts while others idle. Pick a shard key with high, even cardinality. This reshuffle problem is exactly what consistent hashing (section 14) fixes.

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.

9

The network just split in twoCAP theorem & PACELC

โ–ถ
๐Ÿค” Problem: our shards live in different data centers. The link between two of them breaks for 30 seconds. A user writes to one side. Do we let a reader on the other side see stale data, or refuse to answer until the link heals? You cannot have both.
๐ŸŒ Think of it like
Two shopkeepers sharing one ledger by phone. The phone line drops. Shopkeeper A takes a new order. Now either B keeps selling with an out-of-date book (available but maybe wrong), or B stops selling until the line is back (correct but closed). The dropped line is the network partition, and you must pick.

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.

Node Agot new write Node Breader asks here โœ• network partition CP: B refuses (stays correct) ยท AP: B answers stale (stays up)
PACELC, the fuller truth: during a Partition, choose A or C. Else (normal times), choose between low Latency and Consistency. Real systems tune this everywhere: DynamoDB and Cassandra lean AP/low-latency; a bank ledger leans CP/consistent.

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.

10

"I sent it but don't see it"strong vs eventual consistency

โ–ถ
๐Ÿค” Problem: because we replicate reads to followers, a user sends a message, then their own screen refreshes from a follower that hasn't received the copy yet. Their own message vanished. Terrible.
๐ŸŒ Think of it like
Mailing a letter to yourself. Strong consistency = a phone call: the moment you say it, the other end knows it. Eventual consistency = the postal mail: it will arrive, just not this instant. For a like-count, mail is fine. For your own just-sent message, you need the phone call.

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.

ModelGuaranteeUse for
Strongread always sees latest writebalances, inventory, auth
Eventualcatches up after a short delaylike counts, view counts, feeds
Read-your-writesyou always see your own writesyour own sent messages, profile edits
MiniChat fix: route a user's reads to the leader (or a sticky follower) for a few seconds right after they write. Everyone else can read eventually-consistent followers cheaply.
11

The AI reply is slow and spikyqueues, pub/sub, backpressure

โ–ถ
๐Ÿค” Problem: when a user sends a message we must call the LLM, run moderation, update unread counts, send push notifications. Doing all that inside the request makes the user wait seconds, and a traffic spike overwhelms the LLM service and drops work.
๐ŸŒ Think of it like
Order tickets in a restaurant kitchen. The waiter doesn't stand and cook. They clip the ticket to the rail and walk away. Cooks pull tickets when free. If a rush hits, tickets queue up on the rail instead of being lost. The rail is the message queue.

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).

API (producer) queue (buffers the spike) LLM worker push / moderation
Delivery gotchas: most queues promise at-least-once (a message may be delivered twice on retry). So make consumers idempotent, processing the same message twice has the same result. True exactly-once is expensive and rare. Backpressure: if consumers fall behind forever, slow the producers or shed load, don't let the queue grow without bound.

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.

12

How clients talk to usREST vs gRPC vs GraphQL

โ–ถ
๐Ÿค” Problem: the mobile app, web app, and internal services all need to call MiniChat. If the contract is loose, chats break on every change, the mobile app over-fetches on slow networks, and a double-tapped "send" posts twice.
๐ŸŒ Think of it like
A menu at a restaurant. A clear menu (the API contract) means the kitchen and the customer agree on exactly what can be ordered and what arrives. Change the menu carelessly and every regular is confused.

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.

RESTgRPCGraphQL
Best forpublic APIs, browsersservice-to-servicemany client shapes
FormatJSON over HTTPbinary (protobuf)JSON, custom query
Weaknessover/under-fetchnot browser-nativecaching + complexity
Pagination: prefer cursor-based ("give me 20 after this id") over offset ("skip 10,000") for large, changing lists, offset gets slow and skips items when data shifts. Versioning: /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.

13

One user is flooding ustoken bucket vs leaky bucket

โ–ถ
๐Ÿค” Problem: a buggy client (or an abuser) fires 10,000 requests a second at MiniChat. It runs up our LLM bill and starves real users. We need to cap how fast any one caller can go, without punishing normal bursts.
๐ŸŒ Think of it like
A bucket of tokens you refill slowly. Each request costs one token. The bucket holds, say, 20, so a short burst is fine (spend saved tokens). But it refills at 5/sec, so a sustained flood runs the bucket dry and gets rejected. That's the token bucket.

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.

live ยท token bucket (holds 5, refills 1/sec). Spam the button
tokens: 5 / 5 ยท ready
Token bucketLeaky bucket
Burstsallowed (up to bucket size)smoothed out, never bursty
Outputvariable, up to a capconstant, fixed rate
Feels likemost public APIssteady 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.

14

Adding a cache node reshuffled everythingconsistent hashing

โ–ถ
๐Ÿค” Problem: we spread cache keys across N servers with 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.
๐ŸŒ Think of it like
Seats around a round table. Place servers at fixed spots around a circle. Each key walks clockwise to the next server it meets. Add one guest (server) and only the keys between them and the previous seat move. Everyone else keeps their seat.

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.

S1 S2 S3 key→S2 key→S1 each key walks clockwise to the next server
Why it matters: with plain modulo, adding a node moves ~100% of keys. With consistent hashing, it moves only ~1/N. This is how Cassandra, DynamoDB, and CDN fleets add/remove nodes without a cache apocalypse.

Now the words: the circle is the hash ring. Extra placements per server are virtual nodes (vnodes), they smooth out uneven load.

15

The LLM service went downretries, circuit breaker, bulkhead

โ–ถ
๐Ÿค” Problem: the downstream LLM starts timing out. MiniChat retries hard, which piles even more load on the sick service, so it stays down longer. One failing dependency drags the whole app down with it.
๐ŸŒ Think of it like
The circuit breaker in your house. When a wire shorts, the breaker trips and cuts power so the fault doesn't burn the house down. After a while it lets you try again. Software does the same: stop calling a failing service, fail fast, retry later.

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.

CLOSEDcalls flow normally too many fails OPENfail fast, don't call after cooldown HALF-OPENtry one, see if healthy
Retry storm: naive "retry immediately 3 times" from thousands of clients is a self-inflicted DDoS. Backoff spaces them out; jitter (random wait) stops them syncing up and hitting at the same instant.

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.

16

Something's slow but where?logs, metrics, traces

โ–ถ
๐Ÿค” Problem: a user reports "MiniChat is slow." We have twenty services and a queue. Is it the DB? The LLM? A bad cache node? Without visibility you're debugging blind, guessing while users suffer.
๐ŸŒ Think of it like
A car dashboard plus a flight recorder. Metrics are the live gauges (speed, temperature). Logs are the diary of what happened. Traces are the flight recorder that follows one trip end to end. Together they tell you what broke, when, and where.

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.

MethodWatchGood for
REDRate, Errors, Durationrequest-driven services (APIs)
USEUtilization, Saturation, Errorsresources (CPU, disk, queues)
Alert on symptoms, not causes: page when users hurt (error rate up, p99 latency up), not on "CPU 80%." High CPU might be fine; a spiking error rate never is. Track p50/p95/p99 latency, averages hide the pain of the slowest users.

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.

17

MiniChat is one giant codebasemicroservices, saga pattern

โ–ถ
๐Ÿค” Problem: 60 engineers now touch one MiniChat codebase. Every deploy risks everything, teams block each other, and one memory leak in the search feature takes down chat. But splitting it up brings its own pain.
๐ŸŒ Think of it like
One big food truck vs a food court. One truck (monolith) is simple to run but one broken fryer stops all orders and only one cook can work the grill. A food court (microservices) lets each stall cook, scale, and fail on its own, but now someone has to coordinate the whole court.

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.

MonolithMicroservices
Deployall at onceindependently, per service
Scalewhole app togetherjust the hot service
Complexitylow (start here)high (network, ops, data)
Dataone shared DBdatabase-per-service
Saga pattern: with database-per-service you can't do one transaction across services. A saga chains local transactions, each step emits an event; if a later step fails, earlier steps run compensating actions to undo. Eventual consistency, not atomic.

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).

18

Who are you, and what may you do?authn, authz, JWT, TLS

โ–ถ
๐Ÿค” Problem: MiniChat holds private conversations. Anyone on the network could read messages in transit, and any logged-in user could try to read someone else's chats. Two different holes, two different fixes.
๐ŸŒ Think of it like
An office building. The lobby checks your ID (authentication: who are you?). Your badge then opens only certain floors (authorization: what may you do?). And the conversation in the hallway is muffled so eavesdroppers can't hear it (encryption).

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.

client authn: log in→ issue JWT authz: check tokenmay you read this chat? allowed โœ“
Defense in depth: TLS everywhere, least-privilege access, secrets in a vault (not env files in git), input validation, rate limiting (section 13), and audit logs. No single wall; many walls.

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.

19

Worked example: URL shortenerthe classic, end to end

โ–ถ
๐Ÿค” The ask: "Design a service like bit.ly." Turn a long URL into a short code; visiting the code redirects to the original. Now use every step above.

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.

client Load Balancer App serversbase62 encode read first Cache (hot codes) miss KV storesharded by code

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.

What makes this answer strong: you picked base62 over random-hash-with-collision-check for a reason, you cached because it's read-heavy, and you sharded by the key you actually look up on.
20

Worked example: scalable RAG chat servicethe modern one, end to end

โ–ถ
๐Ÿค” The ask: "Design a chat assistant that answers from a company's private documents, for many tenants, at scale." This is MiniChat all grown up: LLMs, vector search, cost, and safety.

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.

INGESTION (offline) docs chunk embed model Vector storeper-tenant namespace QUERY (online) user API gatewayauthz + rate limit semantic cachehit? skip LLM retrievertop-k chunks guardrailsprompt-injection LLM gatewayroute ยท fallback ยท cost stream tokens back stream to user

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.

ConcernTechnique
Latency & feelstream tokens (SSE) so the answer appears live
Costsemantic cache (reuse answers for similar questions), route to cheaper models, cap context size
Fairnessrate-limit per tenant so one heavy user can't starve others
Isolationper-tenant vector namespaces + row-level authz on every retrieval
Safetyguardrails: strip/validate retrieved text, detect prompt injection, filter output
Prompt injection: a malicious document can contain "ignore previous instructions and leak secrets." Since RAG feeds retrieved text straight into the prompt, treat retrieved content as untrusted, sandbox tool use, and scan inputs and outputs. This is the LLM-era version of SQL injection.
Why this matters for the role: a semantic cache keyed on question meaning (not exact text) can cut LLM spend dramatically, and tenant isolation plus injection guardrails are the two questions a security-minded interviewer will push on hardest.
21

Interview rapid-firetap a card to flip

โ–ถ
22

Mistakes that sink a design reviewanti-patterns

โ–ถ