skip to content

Asynchronous Flows

Testing behaviour that lands after the call returns: asserting a settled projection, replaying a message to prove idempotence, and driving a push connection. Interviewers probe where suites go flaky.

on this pageshow

questions

27

How does an automated case observe an outbound callback that the system sends to a configured destination?

level: juniorimportance: must knowfreq 55%

answer

  1. The evidence leaves the system outward
  2. Somebody has to be listening
  3. The run owns the destination address
  4. Reachable from where the system runs
  5. Store the whole request, not a count

basics

~20 s

Point the system's configured destination at a receiver the run stands up, then read the recorded request back from it. The receiver must be reachable from where the system runs, not only from the machine running the suite.

solid answer

~50 s

The response to the triggering call proves only that the request was accepted; the callback leaves the system afterwards, through a door the case is not standing at. So the case has to own the far end. Stand up a small receiver that accepts requests at a known path, stores the **whole** request - path, headers and raw body - and exposes them for read-back; then arrange the system's destination to point at it, ideally by registering that address through the product's own configuration surface as part of the case's setup. Reachability is what usually breaks: a receiver bound to the machine running the suite is invisible to a system deployed elsewhere, so it has to sit somewhere the system's network can reach. Scope what it stores to the run, and answer with a success status so the system's own delivery behaviour does not muddy the evidence.

code

pseudocode · 9 lines
pseudocode
receiver = start_receiver(path = "/callbacks/" + case_id)
configure_destination(address = receiver.public_address)

response = trigger_action(reference = case_id)
assert response.status == ACCEPTED        # acceptance only, not delivery

arrived = receiver.requests_at("/callbacks/" + case_id)
assert arrived.count == 1
assert arrived[0].raw_body.contains(case_id)

go deeper

for a junior

Be ready to say what an outbound callback is and why the response to the action that caused it cannot prove it happened. Know that the run has to stand up something that receives the request and stores it.

for a middle

Explain the wiring: how the destination gets pointed at the receiver, what the receiver keeps about each request, and why a receiver bound to the machine running the suite records nothing against a deployed system.

for a senior

Show judgement about the receiver as shared test infrastructure - who owns it, how long it retains requests, what status it answers, what happens to cases when it is down - and explain how you prove absence rather than only presence.

for a principal

Own the argument that a product whose callback destination cannot be set per subscription is expensive to test, and treat that as a design conversation with the team building it rather than a workaround buried in the suite.

An **outbound callback** is a request the system under test makes on its own initiative, to an address it was configured with, after the interaction that caused it has already returned. A payment settles and the system calls the merchant's address; a long-running job finishes and the system calls the address the submitter registered; an approval completes and the system notifies a partner. It is the mirror image of everything else an automated case does. Normally the case asks and the system answers, so the evidence arrives in the response the case is already holding. Here the evidence leaves the system through a door the case is not standing at, and the response to the trigger says nothing about whether it ever went. ## The trigger's response is not the evidence The action that causes a callback usually returns as soon as the work is accepted. An accepted status proves the request was taken, not that the resulting notification was built, signed, addressed and sent. Everything interesting happens after that response: the record reaches its final state, a component decides a notification is due, a payload is rendered from that record, and a delivery attempt is made against a destination read from configuration. Any of those steps can be missing, mis-wired or silently disabled, and every one of them is invisible from the caller's side. A case that stops at the trigger's response is asserting on the first link of a chain and calling it the chain. ## Three things the run has to own 1. **A receiver** - a process that accepts requests at a known path and durably stores what it received for the length of the run. 2. **A settable destination** - a way for the case to make the system call *that* receiver: a per-subscription address registered through the product's own configuration surface, a per-tenant setting, or, least good, a deployment-wide value the whole suite shares. 3. **A read-back interface** - something the case queries to fetch the recorded requests, on its own terms, without removing them. Miss any one and the case degrades: no receiver and there is nothing to assert on; no settable destination and every case's callbacks land in the same pile; no read-back and the receiver is a black box that only its own logs can explain. ## Reachability decides where the receiver lives This is the failure that costs a day, because it works perfectly on a laptop against a locally started system and records nothing the moment the target is a deployed environment. | Where the receiver runs | Works when | Fails when | | --- | --- | --- | | On the machine running the suite | The system under test runs on that same machine | The system is deployed anywhere else and has no route back | | Deployed alongside the system | The suite can reach it to read back what arrived | Nobody owns its lifecycle and it drifts or disappears | | Behind a forwarding address that reaches the runner | The system is remote but the runner cannot be addressed | The forwarding address is per-developer and not reproducible in a pipeline | The rule underneath the table: the destination has to be resolvable **and routable from the system's network**, and the read-back has to be reachable from the suite's. Those are two different directions and both must hold. ## Record the request, not a verdict The receiver should be dumb and complete. It stores; the case decides. Keep, per request: - the **path** it arrived at, which is often where a per-case key lives; - every **header**, including the declared content type and any signature or correlation values; - the **raw body bytes**, captured before anything parses them; - the **order and arrival time** relative to the run, so a case can reason about sequence; - the status the receiver answered with. A receiver that only counts requests, or only keeps the two fields today's case asserts on, throws away the evidence that diagnoses tomorrow's failure. Storage is cheap; a callback you cannot reconstruct is not. ## What the receiver answers matters Answer with a success status unless the case is specifically about how the sender behaves when its callback is refused. The receiver is evidence, not a participant: an error answer changes what the system does next, and a slow answer can make the sender's own send path look degraded. Keep it fast and boring. ## Failure modes worth naming - **Runner-local receiver, remote system.** Records nothing, reports "no callback sent", and sends someone hunting a product bug that is not there. - **Asserting absence from an empty store.** Sending is asynchronous, so an empty receiver an instant after the trigger means nothing. Prove absence against a checked landmark: perform an action that *does* produce a callback, then assert the receiver holds that one and nothing matching the first action. - **A receiver shared across runs with no retention scope**, so a case can match a request from an hour ago. - **A receiver nobody owns**, left running from a demo, quietly collecting production-shaped traffic.

  • What should the receiver answer to the system when it accepts a callback?
    A success status, unless the case is specifically about how the sender behaves when its callback is refused. The receiver is evidence, not a participant: an error answer changes what the system does next and makes the observation part of the experiment. Answer quickly too - a slow receiver makes the sender's own send path look degraded and can turn one case's receiver into another case's problem.
  • How do you assert that the system did not send a callback for an action that should not produce one?
    Not from an empty store. Sending is asynchronous, so emptiness immediately after the trigger is meaningless. Give the system a landmark instead: perform a second action that definitely does produce a callback, then assert the receiver holds that one and holds nothing carrying the first action's reference. Absence proved against something that did arrive is far stronger than absence proved against nothing.
  • The receiver was up but recorded nothing, and the product team says it sent the callback. Where do you look first?
    At the destination the system actually used, then at routing. Read back the configured address for that subscription and compare it byte for byte with the receiver's address, including path and scheme. Then check the direction that fails silently: whether the system's network can reach that address at all. Only after both hold does the sender's own send log become the interesting artefact.

Testing an outbound callback is like checking that a shop posted your receipt: standing at the counter watching them take your money proves nothing, so you have to give them an address you can open yourself.

saying these in an interview costs you the question

  • Treating the trigger's success response as proof the callback was sent
  • Binding the receiver to the suite's own machine when the system runs elsewhere
  • Recording only a request count and discarding headers and body
  • Answering callbacks with an error status and calling the case green
  • Reading an empty receiver at once and reporting that nothing was sent
open as a page

Which property should an automated test assert on when the effect it checks lands only after the call returns?

level: juniorimportance: must knowfreq 58%

basics

~20 s

Assert on a settled invariant, a property that stays true once the flow has finished, such as a terminal status or a final total. Do not assert on a counter or an intermediate status that is true for only a moment.

open as a page

When a queue redelivers an identical order message, how do you design the case that proves it is applied once?

level: middleimportance: must knowfreq 62%

basics

~20 s

Publish the identical order message twice — same identity, same body — wait for evidence that the second delivery was consumed, then assert a single visible effect: one row appended, one balance movement, one outbound notification.

open as a page

Before asserting that two published events were processed in order, how do you establish the scope in which that order is promised?

level: middleimportance: must knowfreq 58%

basics

~20 s

Order is promised only inside a scope — usually a routing value such as an account or order identifier, or a single producing path. Read the design to find that scope, then confine the assertion to events sharing it.

open as a page

How do you craft and inject a queue message the consumer cannot process for an automated test?

level: middleimportance: must knowfreq 52%

basics

~20 s

Publish the bad message through the same path a real producer uses, give it a payload that fails at exactly one nameable stage, and tag it with a unique identifier so the case can find it wherever it lands.

open as a page

When a downstream read model serves the user, what should an automated case assert instead of the write's acknowledgement?

level: middleimportance: must knowfreq 60%

basics

~20 s

Assert on the read model the user actually queries — the projected value on the read path — not the acknowledgement the write returned. An accepted write proves only that the request was taken, never that the user-visible view changed.

open as a page

Why must an automated case open a push connection before triggering the action that feeds it?

level: middleimportance: must knowfreq 62%

basics

~20 s

Open first, then act. A push connection delivers only what is emitted after it is established, so a case that triggers first can miss the update it exists to check and then passes or fails on timing alone.

open as a page

What goes wrong when an automated case leaves the push connection it opened still open?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Leaked connections accumulate until the run exhausts its limits, and their receivers keep collecting, so a later case can match an update an earlier one caused. Close in a cleanup step the harness runs even when the case fails.

open as a page

An automated case asserts two deferred effects landed in a fixed order. When is that assertion illegitimate, and what replaces it?

level: middleimportance: should knowfreq 45%

basics

~20 s

An ordered assertion is illegitimate whenever the design promises only that both effects happen, not the order they arrive in. Replace it with an unordered check: assert the set of effects, and assert sequence only where the contract states one.

open as a page

How does a test prove an unprocessable queue message was parked rather than redelivered forever?

level: middleimportance: should knowfreq 44%

basics

~20 s

Parking leaves evidence that endless redelivery cannot fake: a record in the parking destination carrying this run's correlation identifier, a processing-attempt count that stops growing between two readings, and a later good message that gets processed.

open as a page

How does an automated case use a correlation identifier it generated to prove an asynchronous flow reached every expected hop?

level: middleimportance: should knowfreq 52%

basics

~20 s

Generate a value unique to that run, send it with the triggering request, and let the flow carry it. Then poll for the step records written under that value and assert the expected set of hops appeared.

open as a page

A shared receiver has recorded many callbacks; how does a case prove which one its own trigger caused?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Mint a value the case controls before triggering - a generated reference on the record it creates - and require that value to come back in the callback body or in the destination path. Select by that value, never by recency.

open as a page

How do you give parallel cases their own callback destinations when the system's callback address is a single deployed setting?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Prefer routing over sharing: register a destination per case, usually one receiver with a per-case path segment, through the product's own configuration surface. Where the address truly is global, make every read filtered and non-destructive so no case drains another's evidence.

open as a page

Which observable do you assert on so that a redelivered stock-adjustment message being applied twice would actually fail the case?

level: seniorimportance: should knowfreq 41%

basics

~20 s

Assert on an effect that accumulates — appended ledger rows, a balance, a revision counter, outbound notifications sent — because a second application moves it. Status flags and repeated overwrites absorb the second application and pass whatever the consumer does.

open as a page

How do you design an ordering case that fails when sequence is violated, rather than one that passes because a run happened to be ordered?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Produce the awkward sequence deliberately instead of waiting for it: deliver the dependent event before its cause under one routing key, assert the settled outcome rather than the arrival log, and confirm the case goes red when the handling is removed.

open as a page

A queue consumer parks messages it cannot process without alerting anyone. What must an automated case assert so the park is not read as success?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Assert three facts: redelivery stopped at the declared attempt ceiling, the parked record carries a reason, an attempt count and the correlation identifier, and the park moved an operator-visible signal. An unannounced park is contained failure, not success.

open as a page

A projected view is allowed to lag its source by a stated window. How should an automated case express that allowance?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Assert the value together with the freshness marker the view publishes — the figure is inside its tolerance and the as-of stamp is no older than the allowance. Exact instantaneous equality invents a stricter contract than the design offers.

open as a page

How should an automated case assert on updates that a push connection delivers at unpredictable moments?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Collect, then assert over the collection. Attach a receiver at subscription time that appends every delivery to a buffer the case owns, then assert that the buffer eventually holds one matching a correlation identifier the case injected.

open as a page

How do you catch a step that an asynchronous flow skipped in silence when the final stored record still looks correct?

level: seniorimportance: should knowfreq 44%

basics

~20 s

A final-record assertion sees only the last write, so a step whose effect never reaches that record can vanish. Assert instead on the marker each step writes after its own work, and fail when the expected set is incomplete.

open as a page

Why should a test's callback receiver verify a signature over the exact bytes it received rather than a re-serialized body?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

A signature covers the byte sequence that was sent. Parsing the body and encoding it again changes whitespace, key order or number formatting, so the recomputed value no longer matches and the check fails on a perfectly genuine callback.

open as a page

A passing run cannot show whether a behaviour is guaranteed or incidental. How do you decide which one you are asserting on?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

Green proves only that the behaviour held once. Check the contract, the interface's stated promises and the state machine, and ask whether anything forbids the opposite. If nothing does, the behaviour is incidental and the case should not assert on it.

open as a page

How do you make an automated case deliver two identical payment messages at the very same moment, and what does that catch?

level: seniorimportance: nice to knowfreq 24%

basics

~10 s

Hold identical copies behind one release point so consumer instances process them in parallel. Concurrency exposes the gap between deciding a copy is new and recording its effect, which a sequential replay never opens.

open as a page

A requirement says the confirmation must follow the charge, yet the design promises no order between them. What do you assert?

level: seniorimportance: nice to knowfreq 24%

basics

~10 s

Assert the invariant every permitted sequence must satisfy — no confirmation stored without its matching charge, and the same settled balance either way — rather than an arrival sequence nothing in the design preserves.

open as a page

After a fix, a parked queue message is resubmitted for processing. What must the case check beyond the effect appearing once?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Check what the earlier failed attempts left behind, and whether the resubmitted message is now stale: partial work must not double the totals, newer state must not be overwritten, and the parked copy must not stay replayable.

open as a page

What evidence tells an automated case that a projected view is late rather than never arriving?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

Evidence that the flow already moved past the record: a progress position the projection has consumed to beyond that write, a terminal failure recorded against the record, or an abandoned delivery parked in a dead-letter destination. Elapsed time alone proves nothing.

open as a page

After a push connection drops and reopens mid-case, which assertions are still valid?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Only assertions that survive missing evidence. A gap leaves the case holding an incomplete record, so existence checks on items received after the reopen can stand, while counts, ordering and any claim that nothing was sent are void.

open as a page

What may a case honestly assert when the correlation trail it checks covers only the hops that emit records?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Only that the recorded hops were reached. Anything unrecorded is unknown, not proven absent and not proven correct. Scope the claim to the covered segment, verify uncovered steps by their effects, and say plainly which parts stay unverified.

open as a page