Infrastructure primitives
Background job queue - at least once, and the retry that doubles it
Why a job queue delivers at least once, and how idempotent workers and dead letters keep retries honest.
What this board gets wrong on purpose
The tension
The effect ledger is written in the same transaction that completes the job, so a worker that dies after the provider call and before that commit leaves no trace - the lease expires, another worker runs the job, and the only thing standing between the customer and a second charge is the provider honouring our Idempotency-Key. Payment APIs do. Most email and SMS APIs do not, so a crash in that window sends the welcome email twice, and nothing on this board can prevent it. The 60 second lease has the same hole from the other side: a job that runs long without heartbeating is handed to a second worker while the first is still working. And the queue is a table on the application's own Postgres primary - at 1,850 jobs a second it is about 5,500 row writes a second and 160 million dead tuples a day for vacuum, so a backlog after an outage competes for the same disk as the checkout it was meant to take load off.
Practise this as an interview
Take Background job queue - at least once, and the retry that doubles it as a timed system design interview on a blank board - on your own, with the requirements to check yourself against, or with a candidate you invite.
Start here
Why this design
Almost every web application grows a background job queue: send the email after signup, generate the invoice PDF, call the webhook, resize the upload. It is the first place most engineers meet a distributed systems guarantee head on, because a worker can crash between doing the work and saying it did. Interviewers use it to see whether you know what "at least once" means, whether you can make a handler idempotent rather than hoping, and whether you have a plan for the job that fails every single time.
What you will learn
- Why exactly-once delivery is not on offer, and why at-least-once plus an idempotent handler is the honest substitute.
- Claiming with FOR UPDATE SKIP LOCKED, leases with a timeout, heartbeats for long jobs, and the reaper that returns abandoned work.
- Enqueueing in the same transaction as the business write, and the crash window it closes.
- Exponential backoff with jitter, a maximum attempt count, and why dead letters are a product feature.
- The one metric that says whether the queue is healthy: the age of the oldest ready job.
How to read this board
- Press Play and follow the walkthrough: ENQUEUE on the left, THE QUEUE, the WORKERS, then SIDE EFFECTS on the right.
- Then the failure paths: backoff and the reaper under THE QUEUE, DEAD LETTERS along the bottom.
- Then OBSERVE on the left - the single number that pages someone.
- 1 · Requirements to 6 · Deployment in the written band, then the DATA MODEL.
- Then argue with the tension: the duplicate this design cannot prevent, and the primary it shares.
1 · Requirements
Ask these before drawing anything
- What do the jobs DO? A job that writes only to our own database can be made exactly-once with a transaction. A job that calls the outside world cannot, and the answer for each provider is different.
- How long does the longest job run? This sets the lease, and a lease shorter than the slowest job is a duplicate generator.
- Is order required? FIFO per customer is expensive and almost never needed. Ask for the actual business rule.
- What is the acceptable delay? "Within a second" and "within an hour" are different worker pools and different alerts.
- What happens to a job that will never succeed? Somebody must look at it. Who, and how quickly?
Functional
- Enqueue a job with a kind, a JSON payload, an optional run_at, a priority and an idempotency key, inside the caller's transaction.
- Workers claim ready jobs, run them, and mark them done; at least once delivery.
- Failed jobs retry with exponential backoff and jitter up to max_attempts, then move to dead letters.
- Abandoned jobs (worker died) are returned to ready when their lease expires.
- Dead letters can be inspected and redriven individually or in bulk after a fix.
- Scheduled and recurring jobs are enqueued by a cron enqueuer with run_at in the future.
Non-functional
- Pick-up latency: p99 under 2 seconds from ready to running at normal load.
- Throughput: 2,000 jobs a second sustained, with headroom to drain a one hour backlog in under 30 minutes.
- No job lost after the enqueueing transaction commits.
- Duplicates bounded to crashes inside the side-effect window, and every handler documents what a duplicate does.
The rest of this board
Available on Tier Slate
This page publishes the question. The answer — 5 more written pages, an 8-step narrated walkthrough and a 5-table schema — is the board itself, and it opens in Tierslate.
- 5written pages
- 8walkthrough steps
- 5tables41 columns
5 pages behind this one
- Capacity estimation
- Storage estimation
- Availability
- How it works
- Deployment plan
More boards
The building blocks an interview asks you to build from scratch.
- URL shortener - one write, a million reads
- Distributed cache - the ring, and what a node leaving costs
- Key-value store - quorums, and where the consistency knob sits
- Rate limiter - token bucket, and where the counter lives
- Unique ID generator - 64 bits, and two facts nobody can check
- Web crawler - a frontier, and the trap of crawling forever
- Pastebin - a blob store with a short name
- Load balancer - spreading traffic, and noticing who is dead
- API gateway - one front door for many services
- Login and sessions - who you are, on every request
- Feature flags - a config change that ships like code
- Secrets manager - the one store everything trusts
- DNS - the cache that runs the internet
- Distributed cron - exactly one runner at nine o'clock
- Code execution sandbox - running a stranger's program
- Distributed log - Kafka from the partition up
- Consensus - a leader, a log, and a majority
- Object storage - eleven nines from cheap disks