LearnastraTHE DESIGN COURSE

Make the right trade-off / CHAPTER 36

Synchronous versus asynchronous communication

Decide what must finish before responding and make delayed work visible, durable, and retryable.

Intermediate1h of deliberate study10 questions

Builds on Functional and non-functional requirements, Back-of-the-envelope estimation.

By the end, you can
  • Distinguish accepting work from completing it.
  • Explain queue backlogs and retry side effects.
  • Use an outbox to close the database-publication gap.

01Who waits for the outcome?

A report takes 45 seconds to produce, but the product wants to acknowledge the request within 300 ms. Decide what that first response promises.

  1. Synchronous completion: the caller waits for the report operation's final result. The response claims completion or reports a failure/uncertain outcome.
  2. Asynchronous acceptance: the service saves a job, returns its identifier quickly and completes it later. The caller checks a status endpoint for the final result.

Accepted must not mean “a function was called in memory.” Under our promise it means enough work was saved to survive a worker restart. It also does not mean the report file already exists.

Nonblocking I/O is a separate programming technique: a thread does other work while disk or network activity waits. The API can still be synchronous if the caller waits for completion. Likewise, a queue does not remove a business dependency: shipping must still wait for required payment confirmation.

02Decoupling creates a lifecycle

Give the report a saved job record containing its owner, parameters, state and output reference. Follow that record:

  1. The API checks permission and parameters. It saves the job and recoverable pending work before replying.
  2. The API returns accepted with job ID 42 and a status URL. The caller now has a way to distinguish pending, running, complete and failed.
  3. A worker first checks for completed work. If the job already records completion, reuse its published output reference. Otherwise, one job-store update records a new claim number and deadline. Other workers cannot take the incomplete job before that deadline; afterward one can claim it with a new number. Expiry does not stop a paused old process.
  4. Each claim writes a separate immutable output object. For job 42, claim 7 writes reports/42/claims/7; a replacement claim 8 writes a different object. Use an object-store write that fails if that key already exists; a retry can inspect that object instead of overwriting it. In one conditional job-store update, publish the output reference and completed state only if the recorded claim number still matches. If claim 8 has taken over, claim 7 cannot publish its reference or overwrite claim 8’s bytes. A separate earlier claim check would leave a race before publication.
  5. The caller checks the authenticated status API. It downloads only a completed result it may access.
  6. Repeated failure reaches a finite repair path. After limited attempts, mark failed or move work for inspection instead of wasting workers forever.

A repeated delivery keeps the same logical job ID and returns its published result. Cleanup may remove abandoned, unpublished objects, but must retain the object referenced by a completed job for its documented result-retention period.

An event says something happened; a queued command asks a worker to do something. Both need saved identity and retry rules.

Question Synchronous result Asynchronous job
What does the first success promise? The required operation has completed. Responsibility has been saved, if that is the documented meaning.
Where can overload wait? Workers, connections and finite request queues. Job storage and increasing job age as well as workers.
How is an uncertain result recovered? Retry or query retained operation state. Query the saved job and its eventual outcome.

03Worked example: report generation

If 60 reports/minute arrive and 50 complete, unfinished work grows by ten/minute. After 15 minutes it adds 150 jobs. A fast acceptance response can hide that growing delay; measure oldest pending age, add verified capacity or accept less work.

Now follow an order and its receipt email

  1. The database saves the order and a pending receipt record together. This pending record is an outbox entry. If the transaction fails, neither is committed; if it succeeds, a crash cannot make the system forget the receipt work.
  2. A relay reads pending entries and sends them to the message system. It marks progress after a successful response, and retries unfinished entries.
  3. The message system accepts a send but its reply is lost. The relay may send the same event again. The outbox prevents forgotten work, not all repeated delivery.
  4. A mail worker sends the receipt. If the provider recognizes a stable send identifier, a retry can resolve the earlier send instead of sending again.
  5. Without that provider support, a gap remains. Writing a local “sent” flag before sending risks no email if the worker crashes. Writing it afterward permits a duplicate if the provider accepted the email before the crash.

Explain that external boundary honestly. A database transaction cannot unsend an accepted email, and a durable queue does not give every external action exactly-once behavior.

Report acceptance, durable work and status are separate boundaries

Report acceptance, durable work and status are separate boundariesSave report job 42 and pending work before telling the caller it was accepted. A worker writes a new immutable object for its claim and publishes its reference only if the job still records that claim number. The status API checks permission and returns the saved pending, completed or failed result. Numbered arrows correspond to the connection key below. All connections are directional.Report clientKeep the jobidentifier for statusreadsSubmission APIValidate parametersand permissionJob databaseStatus, publishedoutput reference andpending workSaved job queueDelivery of job 42can repeatReport workerPublish only if theclaim number stillmatchesStatus APICheck permissionbefore returningstate1234567Report acceptance, durable work and status are separate boundariesSave report job 42 and pending work before telling the caller it was accepted. A worker writes a new immutable object for its claim and publishes its reference only if the job still records that claim number. The status API checks permission and returns the saved pending, completed or failed result. Numbered arrows correspond to the connection key below. All connections are directional.Report clientKeep the jobidentifier for statusreadsSubmission APIValidate parametersand permissionJob databaseStatus, publishedoutput reference andpending workSaved job queueDelivery of job 42can repeatReport workerPublish only if theclaim number stillmatchesStatus APICheck permissionbefore returningstate1234567Report acceptance, durable work and status are separate boundariesSave report job 42 and pending work before telling the caller it was accepted. A worker writes a new immutable object for its claim and publishes its reference only if the job still records that claim number. The status API checks permission and returns the saved pending, completed or failed result. Numbered arrows correspond to the connection key below. All connections are directional.Report clientKeep the jobidentifier for statusreadsSubmission APIValidate parametersand permissionJob databaseStatus, publishedoutput reference andpending workSaved job queueDelivery of job 42can repeatReport workerPublish only if theclaim number stillmatchesStatus APICheck permissionbefore returningstate1234567

Follow the numbered arrows

  1. Report client → Submission APIrequest a report
  2. Submission API → Job databasesave the job and pending work together
  3. Job database → Saved job queuesend the saved pending job
  4. Saved job queue → Report workerdeliver the same job identifier
  5. Report worker → Job databasesave completion and output reference if claim matches
  6. Report client → Status APIrequest the status of job 42
  7. Status API → Job databaseread the saved job and result
Save report jobs before accepting them. Each claim writes a separate output object; a guarded job update publishes its reference, and the status API checks access before returning it.
Trace the diagram in words
  1. Save report job 42 and pending work before telling the caller it was accepted.
  2. A worker writes a new immutable object for its claim and publishes its reference only if the job still records that claim number.
  3. The status API checks permission and returns the saved pending, completed or failed result.

04Keep the critical path purposeful

Separate operations by what the user needs now:

  • Finish purchase checks before returning purchase success. Verify the buyer’s permission and save the stock reservation; a queued intention to check them later is not an accepted purchase.
  • Run suitable reports, email and analytics later when delayed completion fits the product and saved jobs expose the result.
  • Define cancellation by progress. For a pending job, save cancelled state and make workers skip it. For running work, report cancellation requested until the worker confirms a stop. A completed charge needs a separate refund action; an accepted email cannot be unsent by changing job state.
  • Keep job history long enough for a client to resolve an old identifier and for operators to repair failed work.
  • Bound arrivals and retries. A queue absorbs a finite burst, not an unlimited sustained completion promise.

In the interview, stop the worker before its effect, after its effect and after acknowledgment. At each point, show the saved job/result and what another worker does next. That is the mechanism behind reliable delayed work.

DRAW. CALCULATE. EXPLAIN.

Accept quickly, finish reliably

20 minutes · guided practice

Report jobs take 45 s, arrive 80/minute and complete 65/minute. A user expects an accepted response within 300 ms and a visible final result. Calculate ten-minute backlog growth and trace a worker crash after result storage but before acknowledgment.

Your self-check
Compare the worked solution
  1. Arrivals exceed completions by 80 − 65 = 15 jobs/minute. Ten minutes adds 150 unfinished jobs if those rates persist.
  2. Save the job and parameters, then return its ID and status URL within the 300 ms acceptance target. The report itself remains pending.
  3. Write a new immutable output object under the job ID and this attempt’s claim number. In one conditional job-store update, publish its reference and completion only if that claim number still matches; then acknowledge the queue item. An old claim can leave an unused object, but cannot replace the published result.
  4. After a crash before acknowledgment, a repeated delivery looks up the same job ID and returns the reference in its completed record. It does not rewrite the output. Cleanup keeps that published object while the result is retained and can remove abandoned objects.
  5. Track oldest age and reduce accepted work or add tested capacity to meet the separate completion target.

The checkboxes record your own assessment. The worked solution gives you a way to check the reasoning.

RETRIEVE BEFORE YOU REVEAL

Keep the important ideas.

Say the answer aloud, then turn the card over.

CARD 01What does accepting a job promise?
  1. The job and enough work to resume it are saved.
  2. The caller gets a status identifier.
  3. Completion or failure arrives later; the first response does not promise the file already exists.
CARD 02What does an outbox fix?
  1. Save the business change and pending event together.
  2. A relay sends that event later and retries failures.
  3. Receivers recognize repeated event IDs before repeating effects; publication may repeat.
CARD 03Why can a queue still overload?
  1. Arrivals above completions grow waiting count and age.
  2. Limit accepted work and retries or add tested capacity.
  3. Measure final delay; fast acceptance does not mean fast completion.
Practise this topic in the revision deck ↗

CHECK THE REASONING

Can you explain the choice?

Answer every question, then finish for an immediate score and an explanation for each answer. Aim for 80%, then revisit any gaps.

1 / 10Report 42 is accepted under the declared contract. What is now true?
2 / 1060 jobs/minute arrive, 50 complete, for 15 minutes. Extra backlog?
3 / 10Which metric best exposes a delayed report promise?
4 / 10The order commits but direct receipt publication fails. What closes that gap?
5 / 10The relay publishes then loses its acknowledgment. What remains possible?
6 / 10A mail worker sends, then crashes before its local success flag. What is the unresolved risk?
7 / 10A service uses nonblocking I/O but returns only after the result. Which contract is it?
8 / 10Why should a job have a retry limit when it keeps failing?
9 / 10Which effect may normally follow order commit asynchronously?
10 / 10What should cancellation promise?
0 of 10 answered

CLOSE THE LOOP

Explain it without the page.

Summarise the mechanism, one trade-off, and one failure mode. Then mark this chapter complete.

Go to the source