Blog
System design, written down properly
Walkthroughs, interview problems and engineering notes. Every post is built from something that was actually measured or built, not summarised from elsewhere.
Roni Das
Lessons and tutorials at systemdesign.academy
106 posts · page 2 of 6
Design Google Calendar: Recurring Events, Time Zones, Free/Busy and Sync
To design Google Calendar, store each event once, on the calendar of the person who organized it. Give every guest's calendar a small row that points to that event and holds their own answer (yes, no, maybe) and their own reminders. Store a repeating meeting as one rule plus its exceptions. The rule is written in the iCalendar format from RFC 5545, for example RRULE:FREQ=WEEKLY;BYDAY=MO, which means every Monday. An exception is one changed or cancelled date. Keep the rule's start time in the organizer's time zone, such as America/New_York, and not as a UTC time. UTC is the world reference clock that never changes for summer time, so a meeting stored in UTC moves by an hour for local people twice a year. Turn the rule into real dates (instances) only for the time window someone asks for. Also keep the instances for the next 12 months or so in an index, because free/busy searches and reminders need fast lookups by time. Free/busy means the list of times a person is busy. Find a free slot by merging everyone's busy times and looking at the gaps. Send each change to every guest's calendar through a queue, so one slow guest never blocks the organizer. Reminders go into a delayed-job scheduler: a store of jobs sorted by the minute they must fire. Phones stay in sync with a sync token: a bookmark into a per-calendar list of changes, so a phone downloads only what changed since its last visit. Split the data across machines by calendar id.
readPayment System Design: How to Design a Payment System
A payment system takes money from a customer and gets it to a merchant. It never charges twice and never loses a cent. Speed is not the hard part. Correctness under failure is. The network can time out after the bank has already taken the money. A server can crash between two writes. A webhook can arrive twice, or out of order. So the design rests on a few ideas. Every request carries an idempotency key, so a retry is safe. Every money movement is written to a double-entry ledger, where debits and credits always add up to zero. A payment whose result is unknown stays pending. It is never marked failed by guesswork. Events are saved in the same transaction as the ledger write, so none are lost. Every day, the ledger is matched against the payment provider and the bank. Stripe, Razorpay, UPI and PayPal all follow this pattern on different rails.
readBlinkit System Design: 10-Minute Delivery at Scale
Blinkit delivers groceries and daily items in about 10 minutes. It keeps stock in small warehouses near customers. These are called dark stores. Every customer is really shopping one nearby store's shelves. So the core of the design is knowing exactly what each store has, at all times. You also need to pick which store serves an address. You need to hold an item for a customer while they pay. You need to pick and pack fast, and send a delivery partner. Blinkit's own numbers add three harder problems. First, it opened about 900 stores in one year. So opening a store must be a data change, not a code change. Second, stock goes missing, about 1.8% of order value in one quarter. So the system must expect its records to be wrong, and catch it. Third, in September 2025 Blinkit started owning its stock. Before, sellers owned it. This changed the meaning of every stock record while the stores kept selling.
readMakeMyTrip System Design: Flight and Hotel Search
MakeMyTrip is an online travel agency. A traveller searches for flights or hotels, compares them, and books. It looks like search plus booking. But the hard part is where the truth lives. Every seat and every room is in an airline or supplier system. Those systems are slow and cost money to ask. Their answers change all the time. Asking all of them for every search would be too slow and too costly. Showing only saved answers would show prices that no longer exist. So the design is a balance. Save answers so search is fast. Refresh the saved answers smartly so they stay mostly right. Then check the live price again at the moment of booking. That is the moment when being wrong really matters. On top of this sits personal content. One example is showing a user the hotels they looked at recently. MakeMyTrip rebuilt this to respond in tens of milliseconds.
readredBus System Design: Seat Booking and Seat Locks
redBus is a marketplace for bus tickets. A traveller searches for buses between two cities on a date. They pick a bus and a seat on a seat map. They pay and get a ticket. It sounds like any booking system. But three things make it hard. First, the seats come from thousands of bus operators. Some run their buses on redBus's own software. Others use their own systems. Second, one seat is one physical thing, but it can be sold in several places at once. Two people must never get seat 14 on the same bus. Third, demand is very uneven. The evening before a festival, everyone wants the same routes at the same time. The main design problem is the seat lock. You hold one seat for one customer while they pay. You release it if they do not pay. And you never sell it twice.
readUPI System Design: How India's Payment Switch Works
UPI is India's real-time payment system. You pay from an app like PhonePe or Google Pay. But the app never holds your money. Your money stays in your bank account. The app talks to a central switch run by NPCI, the National Payments Corporation of India. The switch finds the person you are paying from their UPI ID. It asks your bank to check your PIN and take the money out. Then it asks their bank to put the money in. The banks settle the real money between themselves later, in batches. Speed is not the hard part of this design. The hard part is staying correct when something times out in the middle. For example, your bank took the money, but the other bank never answered. The other hard part is protecting one central switch from its own users. In March and April 2025, UPI went down three times. NPCI found the cause: banks and apps asked 'did this payment go through?' far too often, and far too fast.
readFraud Detection System Design Interview: Real-Time Scoring, Extreme Class Imbalance, Delayed Labels, and Choosing a Threshold That Costs Money
Fraud detection is the classic machine learning system design question, and it is a good one because every hard part of production ML shows up at once. The system scores a transaction in real time, inside the authorization path, and returns approve, decline, or send for review. The scoring itself is the easy half. The first hard part is class imbalance: fraud is a small fraction of transactions, so accuracy is a useless metric and a model that approves everything looks excellent. You talk about precision and recall instead, and better still about the money each mistake costs, because a false negative costs the chargeback and a false positive costs a blocked customer who may never come back. The second hard part is that labels arrive late. A chargeback can land weeks after the transaction, so the training set for last month is not finished yet, and anything you measure today is measured on an incomplete picture. The third is a feedback loop that biases your own data: you only ever learn the outcome of transactions you approved, so the declined ones have no label and the training set slowly stops resembling the traffic. That is reject inference, and the standard answer is to approve a small random slice of transactions the model wanted to decline, and pay for the labels. The fourth is that features have to be fresh. Velocity features like how many times this card has been used in the last ten minutes are the most predictive signals available and they are worthless if they are an hour old, which pushes you into streaming aggregation with a state store rather than a nightly batch job. The fifth is adversarial drift: the attacker adapts, so a model that was accurate in March is not merely stale by June, it has been actively worked around. Finally there is the part candidates forget, which is that a decline often has to be explainable to a regulator or a customer, so a pure black-box score is not a complete answer and most real systems run a rules engine alongside the model. A strong answer treats the model as one component in a decision system, not as the system.
readLLM Inference System Design Interview: Continuous Batching, the KV Cache, GPU Memory, and Why Prefill and Decode Are Two Different Systems
Designing an LLM inference platform means serving a model that is too big to be cheap and too slow to be batched the ordinary way. The starting point is that generation has two phases with opposite characteristics. Prefill reads the whole prompt at once, saturates the GPU's compute, and produces the first token. Decode then emits one token at a time, each step reading the entire model's weights out of memory to produce a single token, which makes it bound by memory bandwidth rather than arithmetic and leaves most of the GPU idle unless you batch. That asymmetry is why naive request-per-request serving wastes most of a very expensive card, and why continuous batching, where the scheduler adds and removes requests from a running batch at every decoding step rather than waiting for a whole batch to finish, is the single highest-leverage thing in the system. The second pillar is the KV cache: the attention keys and values for every token in every active sequence, held in GPU memory so each new token does not recompute the past. It grows with context length and with concurrency, it lives in whatever memory the weights did not take, and it is the real limit on how many concurrent users a card supports. Managing it naively fragments memory badly, which is the problem PagedAttention solves by allocating the cache in fixed-size blocks the way an operating system pages memory. From there the design opens out into the decisions interviewers actually probe: how to split a model that does not fit on one GPU, whether to quantize and what it costs in quality, when speculative decoding helps and when it wastes compute, how to cache shared prompt prefixes, and how to route requests so that one user asking for a 30,000-token summary does not stall a hundred users asking short questions. The metric conversation matters as much as the architecture, because throughput and latency genuinely conflict here: bigger batches raise tokens per second and raise the time each individual user waits between tokens. A strong candidate separates time to first token from inter-token latency from total throughput, and picks which one the product is actually selling.
readMulti-Agent System Design Interview: The Reliability Math, When a Second Agent Helps, and Why Most Designs Should Be One Agent With Better Tools
A multi-agent system is several language-model-driven loops that pass work between each other, and the interview question is almost never how to wire them up. It is whether you should. The core issue is compounding reliability: an agent that takes the right action 95 percent of the time completes a twenty-step task about 36 percent of the time, and splitting that work across agents does not reduce the number of steps, it adds coordination steps on top. Every handoff also loses information, because what one agent knows lives in its context window and what it passes on is a summary, so the receiving agent is working from a lossy copy of the situation. Those two effects are why a single agent with better tools beats a committee of agents in most real systems: replacing five reasoning steps with one well-designed tool call removes five chances to fail, and it is the highest-value move available. There are genuine exceptions and they have a recognisable shape. Parallel research over disjoint sources is one, because the subtasks do not need each other's context and the fan-out is a latency win. A hard security boundary is another, because an agent that handles untrusted input should not be the same agent that holds the credentials. Cost and latency both fan out with the number of agents, and so does the debugging surface: a failure in a five-agent system requires reconstructing five conversations to find where it went wrong, which makes tracing and durable execution structural requirements rather than nice-to-haves. The strongest interview answer starts with the arithmetic, proposes the single-agent design, names the specific conditions under which it would split, and treats the orchestration as a workflow problem with a model inside it rather than as a conversation between colleagues.
readAPI Design System Design Interview: Designing a REST, GraphQL, or gRPC API That Stays Backward Compatible, Idempotent, and Observable at Scale
API design is the practice of defining the contract between a service and its callers: the resources it exposes, the operations allowed on them, the shape of requests and responses, the error format, and the rules for authentication, versioning, and evolution. The three dominant styles are REST over HTTP (resource-oriented, cacheable, ubiquitous), GraphQL (a single endpoint with a typed query language that lets clients ask for exactly the fields they need), and gRPC (contract-first binary RPC over HTTP/2, fast and strongly typed, ideal for internal service-to-service calls). Most of the hard problems are the same regardless of style. You have to model resources and name them consistently, choose HTTP methods and status codes that mean what they say, decide how to version without breaking existing clients, paginate large collections without offset drift, make write operations idempotent so retries are safe, return machine-readable errors in a standard format such as RFC 7807 problem+json, authenticate and authorize callers, and defend the service with rate limits and quotas. Good API design leans hard toward backward compatibility, predictability, and least surprise, because the cost of a breaking change is paid by every integrator at once and cannot be undone unilaterally.
readDesign a Recommendation System: A Machine Learning System Design Walkthrough
Machine learning system design is a distinct interview format from classic system design. You are still expected to talk about services, storage, and scale, but the spine of the answer is the ML lifecycle: frame the problem, define metrics, get and label data, engineer features, pick a model, train it, evaluate it offline, ship it behind an A/B test, serve it within a latency budget, and monitor it for decay. A strong candidate starts by clarifying the business goal and deciding whether machine learning is even the right tool, since a rules-based or heuristic baseline is often the honest first answer. Then they translate the business goal into a concrete ML objective and separate the offline metric they train against from the online metric the business actually cares about, because those two rarely move together perfectly. The data section is where interviews are won or lost: where labels come from, how to avoid leakage, how to handle class imbalance, and how to prevent the train-serve skew that happens when features are computed one way in the training pipeline and a different way at serving time. That last problem is exactly what a feature store exists to solve. For the model itself, the disciplined move is to establish a simple baseline before reaching for anything complex, and for recommendation, search, and feed ranking (the most common ML design prompt) to use the two-stage candidate-generation-plus-ranking pattern that narrows millions of items down to a few hundred cheaply, then scores those precisely. Offline evaluation tells you whether a model is promising; only an online A/B test tells you whether it actually helps. Serving splits into batch (precompute predictions on a schedule) and online (compute on request), each with different latency and freshness trade-offs. Finally, the system needs monitoring for data drift and model decay, because an ML system that is not watched will silently get worse. The candidates who stand out are the ones who treat the model as the easy part and the platform around it (feature stores, low-latency serving, drift monitoring, retraining) as the hard part, because in production that is exactly where the difficulty and the value live.
readDesign a Time-Series Database: Schema, Write Path, Compression and Scale
In a time-series database, the basic unit is a series. A series is one metric name plus a set of labels. A label is a name and a value that says where the number came from, such as region=eu. So http_requests_total with service=checkout and region=eu is one series. Each series is a list of (time, value) pairs in time order. New points are first written to a write-ahead log: a file on disk that only grows at the end, so a crash cannot lose them. They are also kept in memory, already compressed. Every two hours, the oldest data in memory is written out as a file that never changes again. Compression is what makes the design affordable. Timestamps are stored as tiny differences, and each value is stored as the bits that changed since the last value. Facebook's Gorilla paper took points from 16 bytes to an average of 1.37 bytes this way. An inverted index, a lookup table from each label to the series that carry it, finds the series a query needs. Old data is deleted one whole file at a time. Long-range charts read pre-computed summaries called rollups. What breaks these systems is cardinality: the number of different series. One label with too many values, like a user id, can turn thousands of series into millions.
readDynamoDB System Design Interview: Designing a Distributed Key-Value Store That Serves Millions of Requests per Second at Single-Digit-Millisecond Latency
DynamoDB is a managed distributed key-value and document store descended from the 2007 Amazon Dynamo paper. You address every item by a primary key, and the system hashes that key to decide which partition, and therefore which set of storage nodes, owns the item. Data is partitioned by consistent hashing and replicated to a small number of nodes (typically three) across separate availability zones, so a single node or even a whole zone can fail without losing data or availability. The original Dynamo was fully leaderless and used vector clocks with application-side conflict resolution, but production DynamoDB moved to a per-partition leader with quorum-style replication and a write-ahead log, which is simpler for developers to reason about. Reads come in two flavors: eventually consistent (cheap, may be slightly stale) and strongly consistent (routed to the leader replica). The hard design themes are all trade-offs: how you pick a partition key so load spreads evenly instead of creating a hot partition, how the system reshares and moves data online as tables grow, how secondary indexes are kept up to date asynchronously, and how global tables give you multi-region writes at the cost of last-writer-wins conflict resolution. DynamoDB leans toward availability and predictable latency over rich query power and strong global consistency.
readDesign an AI Agent: Agentic System Design Interview
Wire a model to a few tools and you have something that looks like an agent; run it on a real task and the loop is where every hard problem surfaces. The model does not execute anything itself; it emits a request to call a tool, an orchestrator runs the tool, and the result is fed back for the next decision, over and over. The first thing that bites is reliability, because a chain of steps multiplies its per-step error rate, so the design has to add verification, retries, and bounded scope to stop a long task from almost certainly failing. The second is state, because an agent run is long enough to outlive the process running it, which turns it into a durable-execution problem closer to a workflow engine than to a web request: you record each step so a crash resumes instead of restarting. The third is memory, since the growing transcript is both the agent's working memory and its biggest cost, so you decide what to keep in the context window, what to push to a store and retrieve later, and what to summarize. The fourth is safety, and it is sharper than in a plain chatbot because the agent can act: a tool can send money or delete a file, and the untrusted text a tool returns can carry a prompt injection that hijacks the next decision, so tools run with least privilege and irreversible actions wait for a human. Cost ceilings, loop detection, parallel tool calls, and trajectory-level evaluation ride on top of all of it. Get the control loop and its guardrails right and the agent ships; get them wrong and no starting prompt will save it.
readDesign ChatGPT: LLM Serving System Design Interview
Designing ChatGPT looks like designing a chat app until you notice the model is stateless and the GPU is the entire problem. Every turn resends the whole conversation, so the model reprocesses the full history to produce the next reply, and the cost of a conversation grows as it gets longer. Inference itself splits into two phases with opposite bottlenecks: prefill reads the prompt in parallel and saturates compute, while decode emits one token at a time and is limited by memory bandwidth, because each token has to re-read the model weights. That asymmetry drives nearly every decision. The KV cache that makes decode affordable is also what fills the GPU, so PagedAttention borrows virtual-memory paging to stop fragmentation from wasting most of the card, and continuous batching swaps finished sequences out at each iteration instead of waiting for the slowest one. Prefix caching turns a shared system prompt and a repeated conversation history into a cache hit rather than recomputation. Speculative decoding exploits the fact that verifying several tokens costs about the same as generating one. Then the product layer adds its own constraints: streaming so the answer starts before it is finished, moderation on both the input and the streaming output, token-based quotas rather than request-based ones, and routing that treats a GPU holding long conversations as full even when its request count looks low. The chat CRUD is a weekend project. The interview is about GPU memory, batching, and the latency budget.
readRAG System Design Interview: Retrieval-Augmented Generation from Chunking to Grounded Answers
Retrieval-augmented generation sounds simple, look up relevant text and paste it into an LLM prompt, but nearly every hard decision is hidden in that sentence. You have to split documents into chunks (how big, how much overlap, split on what boundary), embed them with a model you must then use identically at query time, and store the vectors in an index whose parameters trade recall against latency. At query time you decide between pure vector search and a hybrid of dense plus keyword search, whether to rerank the survivors with a slower cross-encoder, and how to fit the best chunks into a fixed token budget without burying the answer in the middle. Then you owe the user a grounded answer: the model must respond from the retrieved context and cite it, not hallucinate, which pushes you into prompt design, guardrails, and evaluation. On top of that sit the operational realities, keeping the index fresh as documents change, re-embedding the whole corpus when you switch models, filtering retrieval by tenant and permissions, and measuring retrieval quality and faithfulness rather than guessing. A strong answer treats the LLM call as the easy part and spends its time on chunking, retrieval quality, grounding, freshness, and cost.
readAd Click Aggregator System Design Interview: Counting a Firehose Without Double-Charging Advertisers
An ad click aggregator ingests a massive stream of click and impression events, groups them into per ad, per time bucket counts, and serves two audiences from the same data: advertisers watching a near real time dashboard and a billing system that needs numbers it can defend. The core of the design is a durable log (Kafka) that absorbs the firehose and partitions events by ad id, a stream processor (Flink or Spark Structured Streaming) that runs windowed aggregation over event time, and an OLAP store (Druid, Pinot, or ClickHouse) that holds pre aggregated rollups and answers dashboard queries in well under a second. The hard parts are all about correctness under failure. Events arrive out of order and late, so you need watermarks and a grace period to decide when a minute is done. A click must be counted exactly once, so every event carries a stable id and gets deduplicated. A viral ad creates a hot partition that one consumer cannot keep up with. And because streaming counts can drift, most real systems keep a batch reconciliation path (or a replayable single stream) that produces the authoritative billing number a few hours later. The interview is really a tour of stream processing done under a billing constraint.
readAirbnb System Design Interview: Search, Availability, and the Race to Book the Same Listing
Airbnb is a two sided marketplace where hosts list homes and guests search, book, and pay. A guest types a place and a date range, and the system filters millions of listings by location, dates, price, guest count, and amenities, then ranks them with personalization, all in a couple hundred milliseconds. The genuinely hard subsystem is the booking write path: two guests can try to reserve the same listing for overlapping dates at the same instant, and the design must let exactly one of them win. That means a strongly consistent reservation store, usually a relational database sharded by listing, with row level locking or optimistic concurrency on the calendar, plus short lived holds during checkout. Search runs off a separate denormalized index such as Elasticsearch that is updated asynchronously from booking and calendar events, so search is eventually consistent by design while reservations are not. Payments split a guest charge from a delayed host payout and hold funds until after check in, which forces careful idempotency so a retried request never charges twice. The whole thing is a study in choosing where to pay for strong consistency and where eventual consistency is fine.
readAmazon S3 System Design Interview: Building Object Storage with Eleven Nines of Durability
Object storage exposes a dead simple contract: PUT an object under a key in a bucket, GET it back byte for byte, and never lose it. Underneath, that contract hides two genuinely hard systems. The first is the durability engine that takes each object, splits it into shards using Reed-Solomon erasure coding, and scatters those shards across many disks, racks, and availability zones so that no correlated failure can destroy more shards than the parity can rebuild, all while background scrubbers continuously read, verify checksums, and repair. The second is the metadata index that maps a key to the physical location of its shards, which must stay fast and consistent while holding entries for trillions of keys and absorbing request storms concentrated on a few hot prefixes. On top of those sit a stateless front-end router fleet, a chunked data path with multipart upload for large objects and range reads for partial GETs, and a lifecycle engine that migrates cold data into cheaper tiers. Since 2020 S3 has also offered strong read-after-write consistency, so a read that follows a successful write always sees the latest bytes. The design lesson is that availability comes from stateless replaceable front ends, and durability comes from spreading redundant coded data as widely as possible and never trusting a disk to tell you the truth about itself.
readApache Kafka System Design Interview: Building a Distributed Message Queue on a Commit Log
Kafka is a distributed, partitioned, replicated commit log dressed up as a messaging system. A topic is split into partitions, and each partition is an append-only, totally ordered sequence of records addressed by a monotonically increasing offset. Producers append to the tail, consumers read forward at their own pace and track their own offset, and the same records can be re-read by many independent consumer groups because reading does not destroy the message. Ordering is guaranteed only within a partition, which is the price you pay for horizontal scale. Durability comes from replicating each partition to several brokers, with one leader taking all reads and writes and followers pulling to stay in sync. The in-sync replica set plus the acks setting lets you dial the tradeoff between latency and how many failures you can survive without losing an acknowledged write. The whole thing is fast because it leans on sequential disk I/O, the OS page cache, and zero-copy transfer rather than clever in-memory structures. Coordination that used to live in ZooKeeper now lives inside Kafka itself through the KRaft metadata quorum.
read