Right now, when a customer taps "Place order" in Tiffin, the server does everything before it answers. It checks the cart, charges the card, saves the order, emails a receipt, sends an SMS and updates the restaurant's stats. Only then does the spinner stop.
That worked when Tiffin was small. But one of those steps is slow, and another fails now and then, and the customer pays for both. In this lesson you'll learn to ask one question about every step in a request: does this have to happen before the response?
What happens when you tap "Place order"
Here is Tiffin's checkout today, with typical timings measured on a busy evening:
| Step | Typical time | Slowest 1% (p99) |
|---|---|---|
| Validate cart and prices | 5 ms | 20 ms |
| Charge the card (payment provider) | 800 ms | 2.5 s |
| Save the order | 10 ms | 40 ms |
| Email the receipt (email provider) | 1.5 s | 8 s |
| Send an SMS ("Order placed!") | 600 ms | 3 s |
| Add loyalty points | 15 ms | 50 ms |
| Update the restaurant's daily stats | 20 ms | 80 ms |
Add up the typical column and the customer waits about 3 seconds. On a bad request, when the email and SMS providers are both slow, they wait over 13 seconds. Most of that time is spent on things the customer doesn't need yet. Nobody reads their receipt email while the checkout spinner is still turning.
The question to ask about each step
For every step in a request, ask two things:
- Does the customer need the result of this step to see the answer?
- If this step fails, should the whole request fail?
If both answers are "no", the step can happen later. Let's go through checkout:
| Step | Needs it now? | If it fails, fail the order? | Decision |
|---|---|---|---|
| Validate cart | Yes, wrong prices mean a wrong charge | Yes | Now |
| Charge the card | Yes, "order placed" means "you've paid" | Yes | Now |
| Save the order | Yes, it's the order | Yes | Now |
| Email receipt | No | No, send it later | Later |
| SMS | No, the app already shows the order | No | Later |
| Loyalty points | No, nobody checks within a second | No | Later |
| Restaurant stats | No | No | Later |
Three steps stay. Four move out. Checkout drops from about 3 seconds to about 0.8 seconds, almost all of it the card charge.
Work that must finish before you respond is synchronous. Work you promise to do soon after, without making the user wait, is asynchronous.
Notice "promise". Moving work out of the request doesn't mean it becomes optional. The receipt must still go out. You've changed when, not whether.
Slow and broken dependencies spread
Speed is the obvious win. The less obvious one is that you stop borrowing other systems' problems.
When the email provider is part of checkout, its bad day is your bad day. If it takes 8 seconds, checkout takes 8 seconds. If it is down, you have two bad choices: fail the order (and lose the sale) or ignore the error (and lose the receipt, silently).
There's also simple maths here. Say checkout depends on five outside services and each is up 99.9% of the time. If all five must work for checkout to work:
0.999 × 0.999 × 0.999 × 0.999 × 0.999 ≈ 0.995
That's 99.5%, or about 3.6 hours of failed checkouts a month (0.5% of 720 hours), even though every single provider kept its promise. Each synchronous dependency you remove takes its downtime out of that sum.
Slow steps also hold resources. A server thread or connection waiting 8 seconds on an email provider isn't serving anyone else. At dinner time, a slow provider can fill every worker slot on your app servers and make the whole app slow, not just checkout.
What "later" costs you
Asynchronous work isn't free. Before you move a step out, be honest about what you're signing up for:
- Somewhere to keep the work. "Send this receipt soon" has to be written down in a place that survives a server restart. That's a queue, covered in the next lesson.
- Something to do the work. A separate process, a worker, picks it up. That's another thing to deploy, monitor and scale.
- Failures nobody sees. In a synchronous request, an error shows up on the customer's screen and in your error rate. In a background job, the error happens quietly 30 seconds later. You need retries, alerts and somewhere for failed jobs to go.
- A short gap where things disagree. For a few seconds the order exists but the loyalty points don't. Usually fine. Sometimes it confuses users ("where are my points?"), and you need to design the UI around it.
- Jobs that run twice. Most queues will occasionally deliver the same job twice. A duplicate receipt is annoying. A duplicate payout is expensive. We'll fix this in "Jobs that can run twice".
That's real machinery. So "later" should be a decision about each step, not a habit of throwing everything into the background.
The hard case: charging the card
The card charge is the slowest step left, at 800 ms. Could it be asynchronous too?
Some apps do exactly that. You tap "Pay", the app says "Payment processing", and a minute later you get "Order confirmed" or "Payment failed". UPI payments in India often work like this, because the bank's confirmation can take a while.
For Tiffin, think about what happens next. The kitchen starts cooking when the order arrives. If the payment then fails, a restaurant has cooked a biryani nobody paid for. So the customer has to know "placed or not" before the order reaches the kitchen. Keeping the charge synchronous is the simpler, safer choice here: 800 ms of waiting buys a clear answer.
Keeping the charge and the order in step when one of them fails partway is its own hard problem. We'll take it on properly in Module 16.
Slow work that a user asks for
Not every background job hides behind a fast response. Sometimes the user asks for the slow thing directly:
- A restaurant uploads 40 menu photos that need resizing into four sizes each.
- Finance downloads a CSV of every order last quarter.
- A restaurant asks for a PDF statement of last month's earnings.
Here you can't pretend it's done. The usual pattern is to accept the request, start a job, and give the user a way to check on it:
POST /reports/monthly-statement → 202 Accepted
{ "job_id": "rep_8812", "status": "queued" }
GET /jobs/rep_8812 → 200 OK
{ "status": "done", "url": "https://..." }
202 Accepted means "I've got your request and I'll work on it", which is exactly the truth. The client polls the job's status or gets told when it's ready (Module 6 showed how a server can push that news).
Clock-driven work
A third kind of background work isn't triggered by a request at all. Paying restaurants every Monday, cancelling orders the kitchen never accepted, clearing out abandoned carts. These are driven by the clock. They share most of the machinery with request-driven jobs, plus a few problems of their own, covered in "Scheduled jobs".
Sort the rider app's steps
When a rider taps "Delivered", the server: marks the order delivered, tells the customer's app, asks the customer for a rating (push notification), adds the delivery fee to the rider's earnings, and recalculates the rider's acceptance score for the week. Which steps must happen before the rider's app gets its response?
Show one answer
Now: mark the order delivered. The rider's app needs to know it worked before it shows the next job, and if this fails the rider should tap again.
Later: everything else. The customer update, rating push and score recalculation don't change what the rider sees. Earnings can also be later, but the rider app probably shows "earnings today", so the job should run within seconds, and the UI can show the new delivery as "pending" until it does.
A good follow-up question for the product team: is it acceptable for earnings to lag by a minute during an outage? If not, crediting earnings might belong in the same database transaction as marking it delivered, since it's a fast local write anyway.
What to remember
- For each step in a request, ask: does the user need its result now, and should its failure fail the request? Two "no"s mean it can happen later.
- Moving work out of the request cuts latency and stops slow or broken providers from becoming your outage.
- Each synchronous dependency multiplies into your availability. Five services at 99.9% give you about 99.5%.
- Asynchronous work needs a queue, workers, retries, monitoring, and jobs that are safe to run twice. Don't move work out by habit.
- Keep a step synchronous when the next thing depends on its answer, like charging the card before the kitchen cooks.
- For slow work the user asks for, return
202 Acceptedwith a job ID and let them check back.