System design sample solution

System design: deliver webhooks to merchants

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

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

The system design question

Design the system that sends webhooks to merchants. When something happens in a merchant's account, like a payment succeeding, send an HTTP request to the URL they've registered. Deliver reliably, even when their servers are slow or down.

Agree on the scope (5 minutes)

Say up front what you're covering.

We'll designWe'll skip
Recording eventsThe payment system itself
Sending them to merchants' URLsDeciding which events exist
Retrying failuresThe dashboard design
Letting merchants see what was sentBilling

Then agree on what "good" means:

  • Never lose an event: every event gets delivered or clearly marked as failed.
  • Quick: most webhooks should arrive within a few seconds.
  • Isolated: one slow or broken merchant can't delay anyone else's webhooks.
  • Secure: merchants can tell a webhook really came from us.

Rough numbers (5 minutes)

Made up, but reasonable: 500 million events a day, peaks at 5 times the average, about 10% of first attempts fail, and events kept for 30 days.

~6K
events per second, on average
~30K
events per second at a peak
10%
of first attempts fail
~30 TB
of events kept for 30 days
QuestionRough mathWhat it means
How many requests do we keep open?30K a second × up to 10 seconds eachUp to 300K slow requests in flight, so workers can't wait on each one
How much retry traffic?10% of 30KAbout 3K retries a second on top of new events
How much storage?500 million × about 2 KB × 30 daysAbout 30 TB, so it'll need splitting up

The overall design (10 minutes)

Stripe servicessomething happensEvent logsaved firstDelivery queueper merchantDelivery workerssign and sendMerchant serverreturns 200
Events are saved first, then delivered by workers. Failures go to a retry schedule.
  1. A service saves an event, like 'payment succeeded', in the same database update as the change itself.
  2. A small job publishes saved events to a delivery queue, split up by merchant.
  3. Delivery workers sign each event and POST it to every endpoint the merchant has set up for that event type.
  4. Every attempt is recorded, along with the merchant's response.
  5. Failed attempts go to a retry schedule, with longer and longer gaps between tries.

What the apps need to do

ActionWho does itWhat's sent
Register an endpointMerchantURL and which event types they want
List events and attemptsMerchantFilters like time range, type and status
Resend an eventMerchantEvent ID
The webhook itselfUs, to the merchantEvent ID, type, data and a signature header

What we store and where

DataWhere it goesWhy
EventsDatabase, split by merchantThe permanent record, kept for 30 days
EndpointsRelational databaseSmall, and read on every delivery, so cached
Delivery attemptsDatabase, split by merchantSo merchants can see what happened
Pending retriesA schedule ordered by next try timeSo workers know what's due

Step 1: Never losing an event (10 minutes)

If a service saves a payment and then crashes before sending the webhook, the event's gone. So save the event in the same database update as the payment change. A separate job then publishes saved events to the queue. It might publish one twice, but it'll never lose one.

That means merchants can get the same event twice. Every event has a unique ID, and merchants should skip IDs they've seen. That's idempotency, and Stripe's docs tell merchants to do exactly that.

Step 2: Delivering without getting stuck (15 minutes)

Workers sign each request with the merchant's secret, so they can check it's really from us. That's standard webhooks practice. Give each request a timeout of about 10 seconds.

The big risk is one slow merchant. If all workers are stuck waiting on them, everyone else waits too. So split delivery by merchant, a lot like sharding, and cap how many requests can be in flight to any one merchant. A slow merchant only slows themselves down.

Workers send requests without waiting on each one, so a single worker can have thousands in flight.

Not overwhelming merchants

After an outage, a merchant might have thousands of events queued up. Sending them all at once could knock them over again. Rate limit delivery per endpoint and ramp up slowly.

Step 3: Retries (5 minutes)

When an attempt fails, schedule the next one further and further out: a minute, then 5, 30, a few hours, and so on, for about three days. Add a little randomness so retries don't all land at once.

After the last try, mark the event failed, email the merchant, and let them resend it from the dashboard. If an endpoint fails constantly for days, turn it off and tell them.

Choices you should be able to defend

We choseInstead ofBecauseWe'd reconsider if
Delivery split up by merchantOne shared queueOne slow merchant can't hold up everyone elseWe had few merchants and they were all reliable
Saving events with the changeSending webhooks right after the changeA crash can never lose an eventThe extra write cost too much
At least once, with event IDsExactly onceExactly once isn't really possible over the internetNever
No ordering guaranteeStrict ordering per objectOrdering would let one failure block everything behind itMerchants really needed order, and could live with delays

What could go wrong (5 minutes)

What happensWhat we do about it
A merchant's server is down for daysKeep retrying for about three days, then mark failed and tell them
A merchant's server is really slowTheir in-flight cap fills up, and only their webhooks wait
A worker crashes mid-sendThe event's retried, and merchants skip the duplicate by its ID
The queue falls behindEvents are saved, so nothing's lost. Add workers
A merchant rotates their signing secretSign with both old and new secrets for a while

Follow-up questions (5 minutes)

What if a merchant needs events in order?

Include a timestamp and version in each event, and tell merchants to fetch the latest state from the API rather than trust arrival order.

How would merchants replay old events?

Let them pick a time range in the dashboard, and queue those events again at a gentle rate.

What about really big events?

Send a small event with the ID and type, and let merchants fetch the full object from the API.

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–30Never losing an event
30–45Delivering without getting stuck
45–50Retries
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 Stripe.