skip to content

A request is in flight when the handset switches connection type - what must the case control and assert?

level: seniorimportance: should knowfreq 45%

answer

  1. The transport dies mid-call
  2. The client cannot tell what the server did
  3. Do not guess the window, signal it
  4. Hold the response, switch, then release
  5. Assert the screen and the server-side count

basics

~20 s

Control the moment: hold the response at a stand-in the run owns, switch the connection, then release. Assert the ambiguous outcome resolves - one definite result on screen and exactly one matching effect recorded on the server side.

solid answer

~50 s

When the handset moves from a wireless local network to a cellular link, the client's source address changes and the outstanding request-response call dies. The client is now in the **ambiguous state**: it does not know whether the server applied the write. A case that hopes to catch that window with a fixed wait is a coin flip, so build a seam - a stand-in in the request path that holds the response until the run tells it to release. The steps become deterministic: send, wait for the stand-in to report the request arrived, trigger the connection change, then release or drop the held response. Then assert both halves: the screen resolves to **one definite outcome** rather than an endless progress surface, and the server side holds **exactly one** effect for one user action. Entered data must survive either way.

code

pseudocode · 15 lines
pseudocode
case "connection type changes after the write arrives":
    given user is on a wireless local network
    standin.hold_responses_for(path = "/orders")

    tap(place_order)
    await standin.saw_request(path = "/orders", within = arrival_signal)

    connection.switch_to(cellular_link)
    standin.drop_held_response()      # server applied it; client never hears

    expect settled(order_screen, within = app_declared_deadline)
    expect visible(confirmation) or visible(failure_notice)
    expect not visible(progress_surface)
    expect server.orders(for = user).count == 1
    expect form_field(delivery_note).value == typed_earlier

go deeper

for a junior

Be ready to explain why a call cannot survive a change of connection type and why that leaves the client unsure whether the write happened. Recall that reads are cheap to repeat and writes are not.

for a middle

Explain the staging mechanics: signal on the request arriving rather than sleeping, hold the response at a stand-in, trigger the change, then release or drop it. Describe both legs of the scenario.

for a senior

Demonstrate the judgment: assert one definite outcome plus exactly one server-side effect, refuse to pin the retry mechanism, preserve typed data, and spend the staging cost only on flows that write.

for a principal

Own the tradeoff between fidelity and determinism, and decide how many of these staged cases the team can maintain against exercising the same flows once on hardware before a release.

A connection change mid-flow is not simply a slower request. It is a request whose transport disappears underneath it, and the interesting part is not the network event - it is the ambiguity it leaves the client holding. ## Two things happen at once When a handset moves from a wireless local network to a cellular link, the client's source address changes. Any request-response call already outstanding on the old path cannot complete on the new one: the client sees a broken connection. That produces two separate problems, and cases usually address only the first. 1. **The transport problem.** A call failed. Does the client notice promptly, or does it sit on a dead socket until some long deadline expires while the user stares at a progress surface? 2. **The knowledge problem.** The client cannot distinguish *the request never arrived* from *the request arrived, was applied, and the response was lost on the way back*. Those are the same observation from inside the client and they demand opposite actions. The second problem is the one interviews are actually probing. A read is forgiving - repeat it. A write is not. ## Controlling the moment The window between "the request left the client" and "the response came back" can be milliseconds. A case that sleeps for a guessed interval and then flips the connection is not testing the window; it is sampling it, and it will pass for the wrong reason most of the time. Build a seam instead: 1. Put a **stand-in you control in the request path** - one that can hold a response rather than forward it. 2. Have the case send the request and then **wait for the stand-in to report that the request arrived**. That is a signal, not a guess. 3. **Trigger the connection change** while the response is held. The run drives this the same way it drives any other input. 4. **Release or drop the held response**, choosing which leg of the scenario this case is for. Now the case is deterministic and it can run in a pipeline unattended. It also lets you write the two legs as separate cases - one where the write landed and one where it did not - which is what makes the assertions meaningful. ## What the case has to assert | Where the change lands | What the server saw | What the app must do | | --- | --- | --- | | Before the request left the client | nothing | send once on the new path; no user-visible error is required | | After it arrived, before the response returned | the write was applied | resolve to one outcome without producing a second effect | | While a large upload was still streaming | a partial transfer | discard or resume; never report success for a partial send | The assertion set that turns this into a real oracle has two halves, and dropping either one is the classic weak version: - **On screen:** the flow resolves to a single definite outcome - a confirmation or a failure notice naming what to do next - within the app's own declared deadline. An indefinite progress surface is a failure, even though nothing crashed. - **On the server side:** exactly one effect exists for one user action. A case that only checks the screen will happily pass a build that quietly submitted the purchase twice. - **Data preservation:** whatever the app chooses, what the user typed is still there. Losing a filled form to a connection change is a defect the screen assertion alone will not catch. Note what this is *not* asserting: it is not asserting that a retry happened, and it is not asserting a retry count. Those are implementation details of one build, and pinning them makes the case brittle. The contract is about the observable outcome, not the mechanism that reached it. ## Choosing which flows get this case The staging cost is real, so spend it where ambiguity is expensive: - **flows that write** - purchases, submissions, transfers - where a duplicated effect is visible to the user or to money; - **flows with a long single call** - an upload, a media send - where the window is wide enough to be hit in the field constantly; - **the first call after sign-in**, where a failure leaves the user in an unclear state. Read-only screens rarely justify it: the recovery is to fetch again, and an ordinary impaired-link case already covers the waiting behaviour. Skip the temptation to run the scenario on every screen; a handful of well-staged cases on the flows that write will find the defects, and a hundred hopeful ones will mostly find themselves.

  • The app shows a failure notice but the server did apply the write. Is the case red?
    Not necessarily. Reporting failure for a write that landed is honest given what the client knew, and it is far safer than reporting success for one that did not. What must be true is that the user can recover without creating a second effect - the next attempt reconciles rather than duplicates, and the screen eventually reflects reality once the app can see the server again.
  • Why not just assert that the client retried the request after the connection changed?
    Because that pins one build's mechanism instead of the contract. A build may retry, may resume, may prompt the user, or may defer - all acceptable. Asserting the retry makes the case fail on a legitimate refactor while still passing a build that retries into a duplicate effect. Assert the observable outcome and the effect count instead.
  • Does this scenario need a real handset, or can an emulated environment carry it?
    The ambiguity is transport-level, so an environment where the run can change the client's network path deterministically reproduces it faithfully enough for the case. What you lose is the real timing of the handover - how long the client sits before noticing - which is why the flows that write are also worth exercising once on hardware before a release.

It is like posting a letter and then moving house before the reply can arrive. You do not know whether the letter was opened, and sending a second one may get the order placed twice.

saying these in an interview costs you the question

  • Uses a fixed sleep to hit the in-flight window
  • Assumes a failed call means nothing was applied
  • Asserts only the screen, never the effect count
  • Pins a specific retry count as the contract
  • Treats every broken call as a crash to be caught
  • Runs the scenario on every screen instead of writes