System design sample solution

System design: design a book price aggregator

A step-by-step system design answer you could actually give in 60 minutes.

System design interview · Sample 60-minute answer · Not Databricks's actual design · Independent, not affiliated with Databricks

The system design question

Design a service where a user gives an ISBN and a max price. It asks lots of bookstores for their price, picks the best one, and places the order. Some stores are slow or fail. Focus on the API, the data model, and how you handle bad stores.

Agree on the scope (5 minutes)

Say up front what you're covering.

We'll designWe'll skip
The API a user calls with an ISBN and a max priceTaking the user's payment (assume a payment service)
Asking many stores for prices, with a time limitShipping and tracking
Picking a store and placing the orderSearch by title or author
Tracking which stores are slow or brokenEach store's own systems

Then agree on what "good" means:

  • Answers in seconds: the user gets a result within a few seconds, even if some stores never reply.
  • One order, never two: a retry or a crash must never buy the same book twice.
  • Respects the max price: we never order above what the user said they'd pay, shipping included.
  • Gets around bad stores: one slow or broken store can't slow down or break everyone's requests.

Rough numbers (5 minutes)

These are assumptions, made up but reasonable: 200 bookstores we can call, 1 million buy requests a day, and peaks about 5 times the average around big sales.

~12
buy requests a second, on average
~60
buy requests a second at a peak
~12K
store calls a second at a peak
~3 s
time limit for collecting prices
QuestionRough mathWhat it means
How many requests?1 million ÷ 86,400 seconds, then × 5 at a peakAbout 60 a second. Small for us
How many store calls?60 requests × 200 storesAbout 12K calls a second, but each store only sees about 60. Store limits matter more than our own size
How much order data?1 million orders × about 2 KB with every quote × 365 daysUnder 1 TB a year. One relational database copes fine

The overall design (10 minutes)

UserISBN and max priceAggregator APIcreates the requestQuote fan-outasks stores in parallelOrders databaserequests, quotes, ordersBookstoresprice and buy APIs
The API saves the request, the fan-out asks every healthy store for a price, and the best quote under the max price becomes one order.
  1. The user sends an ISBN, a max price and a request ID they made up. We save a purchase request and return its ID right away.
  2. The fan-out asks every healthy store that might stock the book for a price, all at once, and stops waiting after about 3 seconds.
  3. Each answer is saved as a quote. When the time's up, or every store has answered, we pick the cheapest quote that's within the max price.
  4. We place the order with that store using our own order ID as the store's request key, so a retry can't buy twice.
  5. If that store fails, we move to the next best quote. The user polls the request, or gets a callback, to see how it ended.

What the apps need to do

ActionWho does itWhat's sent
Create a purchase requestUser's appISBN, max price, shipping address, request ID
Get a purchase requestUser's appPurchase request ID. Returns its state, the quotes and the order if there is one
Cancel a purchase requestUser's appPurchase request ID. Only works before an order's placed
Get a price (outgoing)Us, to each storeISBN. Store replies with price, shipping and stock
Place an order (outgoing)Us, to one storeISBN, agreed price, address, our order ID as the request key

What we store and where

DataWhere it goesWhy
Purchase requestsRelational databaseISBN, max price, user, request ID (unique per user), and state: collecting, ordering, ordered, failed or cancelled
QuotesSame databaseOne row per store per request: price, shipping, when it came in, and how long it lasts
OrdersSame databaseOne row per attempt to buy: store, price, our order ID, the store's order ID, and state
StoresSame databaseEach store's API details, timeout, rate limit, and whether it's on or off
Store healthIn-memory cacheRecent error rate and response times per store, for the circuit breaker
Price cacheIn-memory cacheRecent quotes by ISBN and store, kept for a few minutes

Step 1: Asking stores without waiting on the slow ones (12 minutes)

Send all the price calls at once, not one after another, and give the whole round one deadline, say 3 seconds. Whatever's back by then is what you choose from. A store that answers later is just ignored for this request. Waiting for everyone means your speed is the speed of your worst store.

Each store also gets its own short timeout and its own small pool of connections. That way one store that hangs can't use up every connection and block calls to the others. People call this a bulkhead.

Keep a circuit breaker per store. If a store's error rate or timeouts jump, stop calling it for a minute, then let a few test calls through. If those work, turn it back on. This saves time on every request and stops us hammering a store that's already struggling.

Respect each store's limits with rate limiting on our side. Popular ISBNs get asked about a lot, so a short cache of recent quotes, a few minutes long, cuts calls a lot. Prices change, so the order step always checks the price again.

Which stores to ask

Some stores publish a stock list or tell us when they don't carry a book. Remember 'not stocked' answers for a while and skip those stores next time. It's an easy way to cut calls without missing much.

Step 2: Placing the order exactly once (10 minutes)

Two things can cause a double order: the user retrying, and us retrying the store after a timeout. The user's request ID handles the first. It's unique per user in the database, so a repeat gets the existing purchase request back instead of a new one. See idempotency.

For the store, write the order row with our own order ID and state 'placing' before calling. Send that ID as the store's request key. If the call times out, we don't know if it worked, so we retry with the same ID, or ask the store about that ID. We only move to the next store once we're sure this one failed.

The purchase request is a small state machine: collecting, then ordering, then ordered or failed. Each step only moves forward, and the update checks the current state, like 'set ordering only if it's still collecting'. Two workers can't both start ordering for the same request.

Step 3: The API and the data model (8 minutes)

Make creating a purchase return fast, with an ID, and let the user poll it or get a callback. Collecting prices and ordering can take a few seconds or more, and you don't want a phone holding a connection open while you retry a store.

Keep quotes as their own rows rather than squeezing them into the request. That lets you show the user what each store offered, explain why one was picked, and later work out which stores are usually cheapest or slowest.

A relational database fits well here. The data's small, it's all linked by IDs, and the state changes need simple transactions. See relational vs non-relational. Store prices as whole cents with a currency, never as floats.

Choices you should be able to defend

We choseInstead ofBecauseWe'd reconsider if
One deadline for all quotesWait for every storeUsers get an answer in seconds, and one slow store can't hold everyone upThe cheapest stores were reliably the slow ones, so a few extra seconds saved real money
Async API with polling or a callbackOne long blocking callOrdering can take a while with retries, and a blocking call ties up connectionsAlmost every request finished in under a second
Short price cacheAlways ask liveFar fewer calls to stores for popular booksStores changed prices every few seconds, so cached quotes kept failing at order time
Relational databaseA key-value storeSmall linked data with state changes that need transactionsVolume grew far past what one database could hold, which is unlikely here

What could go wrong (5 minutes)

What happensWhat we do about it
A store never answersThe deadline passes and we pick from the stores that did. Its circuit breaker counts the timeout
A store returns nonsense, like a price of 0Check every quote's shape and range, drop bad ones, and count them as errors for that store
The chosen store refuses the order or the price went upMark that order failed and try the next best quote that's still within the max price
Our service crashes while orderingThe request is left 'ordering' with an order row. A sweeper checks with the store using our order ID and finishes it
No store is under the max priceThe request ends as failed with a clear reason, and the user can see the quotes we got

Follow-up questions (5 minutes)

What if the user wants the book whenever the price drops under their max?

Keep the request open as a watch. Re-check prices on a schedule, more often for popular books, and order the first time a quote fits.

How do you add a new store?

Write an adapter that turns their API into our quote and order shapes, and add a row to the stores table. Start it with a low rate limit until its health looks good.

What if we have to charge the user before ordering?

Hold the max price on their card first, order, then capture the real amount. If every store fails, release the hold.

How the 60 minutes add up

MinutesWhat you're doing
0–5Agree on what you're building
5–10Rough numbers
10–20Draw the overall design
20–32Asking stores without waiting on the slow ones
32–42Placing the order exactly once
42–50The API and the data model
50–55What could go wrong
55–60Follow-up questions

Key concepts used here

Further reading

We wrote this sample answer ourselves.

Practice Loop is independent and isn't affiliated with or endorsed by Databricks.