skip to content

How would you standardise submission handling across dozens of forms in one application, and what must stay per-form?

level: principalimportance: should knowfreq 44%

answer

  1. one submission, one state object
  2. mechanism shared, meaning local
  3. guard and request key belong together
  4. escape hatch before the third flag
  5. measure duplicates and silent rejections

basics

~10 s

Share the submission's unit-of-work state, the in-flight guard, the server-error-to-field-path mapping and the announcement behaviour. Leave the payload mapping, the field rules, the copy and the post-success policy with each form.

solid answer

~50 s

I would make the submission a **unit of work** owned by one shared primitive: idle, pending, succeeded or failed, carrying its result and its error, with the synchronous in-flight guard, the request key, the translation of a rejection into field-path-keyed messages plus a form-level fallback, and the focus and live-region behaviour on failure. That is where consistency actually pays — latency feel, wording of transport versus rejection failures, and duplicate protection stop being per-developer decisions. Each form keeps what is genuinely local: how its fields become a payload, its own rules, its copy, and its post-success policy — reset, keep and re-baseline, or replace the screen. If the framework already tracks submissions as one unit, I adopt it behind a thin adapter so forms do not depend on it directly. And I would measure it: duplicate-write rate and unmapped-error rate tell me whether the shared piece works.

go deeper

for a junior

Notice that every form in a product repeats the same submission steps, and that copying that code per screen is how behaviour drifts. Know the vocabulary: pending, result, error, guard.

for a middle

Describe what a reusable submission piece would hold — one state for the submission, the guard, the failure classification — and which parts obviously cannot be shared, such as payload assembly and copy.

for a senior

Argue the boundary with examples, and show how you would adopt a framework-provided tracked submission behind a thin adapter so forms are not coupled to it directly.

for a principal

Own the tradeoff: consistency versus a shared piece nobody can escape. Set the escape-hatch rule, refuse per-screen flags, and name the metrics — duplicate-write and unmapped-error rates — that decide whether it keeps earning its place.

## Why standardise this at all Submission is where a product's forms are most visibly inconsistent and most expensively wrong. Left to each screen, you get a different pending feel per form, a different wording for "the network failed" versus "the server said no", some forms that lose input on rejection, some that duplicate writes, and rejected saves that are silent because that form's author did not handle an unmapped complaint. None of those are hard problems; they are problems that recur once per form. ## What the shared primitive should own - **One unit-of-work state.** A submission is `idle`, `pending`, `succeeded` or `failed`, and carries its result and its error. One value, not three booleans that can express nonsense like pending-and-failed. - **The guard.** A synchronous in-flight marker (or the in-flight request itself) so a second start returns the first rather than duplicating it. - **The request key.** Generated once per attempt, reused by retries of that attempt, retired on settlement. - **Failure classification.** Transport failure, rejection with field complaints, rejection without them, conflict, authorisation loss, and the different treatment each deserves. - **Server-error mapping.** Rejections become messages keyed by payload path, with everything unmatched surfacing at form level rather than disappearing. - **Announcement and focus.** A live region for start and settlement; focus to the first offending field in document order, or to the summary. - **Latency behaviour.** The delay before a spinner appears, the threshold at which the interface says the work is slow, and what it offers then. ## What must stay with each form 1. **The payload mapping.** How this form's fields become the request body, including coercion and the values no control carries. This is the part that is genuinely different every time. 2. **The rules.** Which client-side rules exist and when they run. 3. **The copy.** Field labels and messages that are specific to this domain. 4. **The post-success policy**, because it follows the task and not the mechanism: - a create-another flow resets the fields; - an edit form keeps the values and re-baselines what counts as dirty, since the values are now the saved truth; - a one-shot flow replaces the form with a result; - a step in a longer flow hands off and lets the next step own the screen. | Concern | Shared | Per-form | |---|---|---| | Pending, result, error as one state | yes | no | | In-flight guard and request key | yes | no | | Rejection to field-path mapping | yes | the path vocabulary for its own payload | | Focus and announcement on failure | yes | the wording | | Payload assembly and coercion | no | yes | | What success does to the screen | the settlement point | the policy | ## Adopting what the framework already offers Where the framework tracks a submission started through a form as one unit — its pending status, its returned result, its error — adopt it rather than re-implementing it: it settles even when the handler throws, and any control can read the status without being handed it. Wrap it in a thin adapter so the forms depend on your submission contract instead of the framework's surface. That adapter is cheap insurance: it is also where you add the parts the framework does not cover, notably the request key and the remount-proof guard. ## The failure mode of over-standardising A shared submission primitive that also owns validation, payload shape, layout and copy becomes an in-house framework no one can opt out of, and the first screen that does not fit gets a flag added for it. Two disciplines keep it honest: - **Escape hatch first.** A form must be able to use the guard and the state without accepting the rest, and one that cannot fit should be able to drop out entirely without deleting the shared piece. - **Additive by policy, never by flag.** New behaviour is a new option with a sane default, and if you are adding the third boolean to accommodate one screen, that screen wants its own path. ## How you know it is working This is measurable, and treating it as a matter of taste is how it regresses: - **Duplicate-write rate** per endpoint — the only real test of the guard and the request key. - **Unmapped-error rate** — rejections that produced no field message, which tells you the payload and the forms have drifted. - **Stuck-pending reports** — forms that never settled, which point at a handler throwing outside the settle path. - **Time-to-settle distribution**, because the latency thresholds you chose are only right for the latency you actually have. The short version to say out loud: share the mechanism and the failure handling, keep the meaning and the copy local, adopt the framework's tracked submission behind an adapter, and put a number on duplicates and on silent rejections so the shared piece has to keep earning its place.

  • Why keep the post-success policy per form rather than configuring it centrally?
    Because it follows the user's task, not the transport. Creating in bulk wants a reset, editing wants the values kept and the dirty baseline moved, a one-shot wants the form replaced. Central configuration for that becomes a list of mode names that each mean something slightly different per screen, so the shared piece should expose a settlement point and let the form decide what happens there.
  • How would you migrate dozens of existing hand-rolled forms onto the shared contract?
    Start where the damage is measurable: the endpoints with duplicate writes and the forms with silent rejections. Convert those, keeping each form's payload mapping untouched so the diff stays small and reviewable. Make the shared primitive the default in the scaffolding and let the rest convert as they are touched; a big-bang rewrite of form code buys nothing that incremental conversion does not.
  • What tells you the shared primitive has grown too large?
    Boolean options added for a single screen, forms importing it only to switch most of it off, and changes to it that require regression-testing unrelated screens. At that point split it: keep the submission state, guard and failure classification in the core, and move the opinionated behaviour into optional pieces a form opts into.

saying these in an interview costs you the question

  • Three separate booleans instead of one submission state
  • Centralising copy and payload shape along with the mechanism
  • Adding a boolean option for each awkward screen
  • No escape hatch from the shared primitive
  • Re-implementing a tracked submission the framework already provides
  • Judging the result by taste with nothing measured