cf.completefrontend
My progress
SYSTEM DESIGN · #128

Design a long insurance application form

An online application for car, home, life or health insurance with 50 to 200 questions: a question graph that grows and shrinks with the answers and is versioned like a legal document, a live price that changes as you answer, prefilled data the customer must confirm, disclosures and consent that are recorded word for word, an audit trail of every answer, and agents who fill the form with the customer on the phone.

The question

Design the online application for our car and home insurance. Depending on the answers it is 50 to 200 questions, the price should update as people answer, and an agent in our call centre must be able to fill it in with a customer on the phone.

We sell in several states and countries, the questions and wording are filed with regulators, and what the customer answers becomes part of the contract. Walk me through how the questions are modelled, pricing, prefill, consent, the audit trail, sessions and security, and accessibility.

Real-life examples: Lemonade home and renters insurance, Progressive and GEICO car quotes, Policybazaar and Acko in India, Admiral car insurance in the UK, Online term life and health applications

Requirements: a form that becomes a contract

Buying insurance online starts with a quote: a price for a policy, based on what you told the insurer. You enter your car, your address, who drives, and your history, and the site shows a monthly price. If you like it, the same answers become an application, and when you pay, the insurer issues a policy: a contract.

Car quotes from Progressive, GEICO or Admiral, home cover from Lemonade, and comparison sites like Policybazaar all follow this shape. A car quote runs to several dozen questions, and a life or health application with medical history can pass a hundred.

This guide is not about the wizard machinery. The multi-step-form guide already covers step state, per-step validation, conditional steps, progress, and a server draft that resumes on another device. The form-builder guide covers rendering forms from a schema. Here we build on both and focus on what is special when the form is regulated and drives a price.

Questions to ask

Each question below changes a decision later in the guide. The middle column is what we assume.

QuestionWhat we assumeWhat it changes
Which products and markets?Car and home, 12 US states plus the UK, more laterThe questions must be data per product, market and date, not code
Who changes the questions?Underwriting and compliance teams, often monthlyWe need a versioned schema and a way to keep old versions alive
Is the price shown while answering?Yes, after the first 10 to 15 rating answersA rating engine call on the critical path, with races to handle
Do we prefill from outside data?Vehicle lookup by registration or VIN, address lookup, prior insuranceConsent before checks, and a confirm step for every prefilled value
Can agents fill it in?Yes, about a third of applications are agent assisted (an estimate)Two kinds of actor, attestation rules, a hand-off flow
What is legally required?Disclosures shown and acknowledged, an e-signature, a record of answersAn audit trail and an immutable snapshot at submit
How sensitive is the data?Driving offences, claims, health conditions, national idsField-level encryption, masking, nothing in URLs, logs or analytics

Functional requirements

  • Render the questions for a product, market and date from server data, including repeating groups (one sub-form per driver) and knockout questions that end the journey politely.
  • Show a live price after enough rating answers, and update it when a rating answer changes.
  • Prefill from third-party lookups after consent, and let the customer confirm or correct each prefilled value.
  • Show required disclosures, record each acknowledgement, and take an e-signature before binding.
  • Record every answer change with who made it and which wording they saw, and freeze a snapshot on submit.
  • Let a call-centre agent or broker fill the application with the customer, and hand it to the customer's own device to attest.
  • Save and resume with session timeouts, resume links and re-authentication.

Non-functional requirements

  • Correctness over speed: the price shown must be the price the server would charge for those answers. A wrong price on screen is a complaint and possibly a regulatory issue.
  • Traceability: for any policy, we can show which questions were asked, in which wording, and who answered, years later. Claims disputes can arrive long after the sale.
  • Responsiveness: every input responds within 100 ms, and a new price arrives in under 1.5 seconds at the 75th percentile (our target, rating engines vary a lot).
  • Accessibility: WCAG 2.2 AA, because a legal and financial form must be usable by everyone, and in many markets it is also a legal duty.
  • Mobile first: for consumer car and home insurance, many quotes start on a phone. Check your own analytics before you assume a split.

What makes a regulated application hard

A sign-up form is long. An insurance application is long and has consequences. Five properties drive every decision in this guide.

  1. The questions are a graph, not a list. Saying you have 3 drivers creates 3 driver sub-forms. Saying yes to prior claims adds a claims table. A knockout question such as "Is the car used for paid deliveries?" can end the journey. The number of questions is only known once you've answered them.
  2. The questions are a legal document. In the US, insurers file rates and policy forms with each state's insurance department, and the wording carries legal weight everywhere. A new version may start on 1 March in Texas only. A customer who started on 27 February must keep seeing the February version.
  3. The answers set a price. Some answers are rating factors: inputs the rating engine uses to compute the premium, like the car model, postcode or age. When one changes, the price changes, and the server is the only place allowed to compute it.
  4. The answers become the contract. If an answer was wrong, the insurer may reduce or refuse a claim later, so it must prove which question was asked, in which words, who answered, and when. In the UK, for example, the Consumer Insurance (Disclosure and Representations) Act 2012 asks the customer to take reasonable care not to make a misrepresentation, and how clear and specific the insurer's questions were counts when judging that.
  5. Several people touch one application. A customer starts on a phone, calls the call centre, an agent continues on a desktop, then the customer signs on their own device. Each change must say who made it.

Compare that with the multi-step-form guide. There, steps appear and vanish, but the step list is code that ships with the app, the price is not involved, and nobody asks who typed an answer. The chapters below take these properties one at a time.

Wireframe of the car quote application on desktop. On the left, a section list: About you and Car are done, Drivers is current, History, Cover and Review are not started. In the middle, the current group, Driver 2: licence, with a licence type of Full UK, years held 3, visible help text, and a Continue button. On the right, the price panel shows 91 pounds a month, quote reference Q-8F3K, valid for 30 days, and an updating state. Below it, Save and exit and a help line.Car quote: DriversSectionsAbout youCarDriversHistoryCoverReviewDriver 2: licenceLicence typeFull UKYears held3help: count full yearsContinueYour price£91 a monthQ-8F3Kvalid 30 daysupdatingsave and exithelp: call us
Progress is shown by section, not by percentage, because the next answer can add questions. The price panel keeps a fixed size so a new price never moves the layout.

The wireframe shows the main screen on a desktop. On the left, the section list: About you, Car, Drivers, History, Cover, Review, with no counts for sections not yet reached. In the middle, the current section with one group of related questions. On the right, the price panel with the monthly price, its quote reference and expiry, and a quiet updating state.

On a phone, the price panel becomes a sticky bar at the bottom that opens a sheet with the details. Notice what is not on the screen: a percentage. "62% complete" is a promise you can't keep when the next answer adds 3 driver sub-forms; the accessibility chapter comes back to honest progress.

Think first

Why can't the client just download the full list of 200 questions and show or hide them with simple flags?

Check your understanding · Exercise 1

Which answers from the interviewer would most change the frontend design of this application? Select all that apply.

The question graph: versioned data, not code

The first big decision is how the questions reach the screen. The naive approach is one React component per section, with the questions written in JSX and the conditions written as if statements. It works for one product in one market.

Then compliance changes the wording of the claims question in Florida from 1 April, and you ship a release. A customer who started on 30 March reloads on 2 April and sees wording they never answered. Worse, an old client in another tab still shows the old wording to someone who started on 2 April. Neither matches what was filed.

Decision Where should the questions and their rules live?

Questions in components

Each section is a component with hard-coded fields and if conditions.

Wording comes from the translation files.

Pros

  • Fastest to start, fully typed, easy to debug.

Cons

  • Every rule or wording change is a release.
  • Can't serve two versions at once to customers who started on different dates.
  • The server must duplicate every condition to validate, and the two copies drift.

Schema in the bundle

Questions and rules are JSON files in the repo, rendered by a generic engine (as in form-builder).

One file per product and market.

Pros

  • Rules are data, readable by non-engineers in review.
  • One renderer for all products.

Cons

  • Still a release per change.
  • Old bundles cached by the CDN and browsers keep serving old rules to new quotes.
  • No way to pin a quote to the version it started on.

Versioned graph from the server

The server publishes immutable schema versions, keyed by product, market and effective date.

A new quote is pinned to the version in effect when it started; the client fetches that exact version by id.

Pros

  • Wording and rules ship without a release, and match what was filed.
  • Old quotes keep their version; new quotes get the new one.
  • The same rules file runs on the server for validation.

Cons

  • Needs a rule language both sides can run, and a renderer that handles every question type.
  • Harder to type-check; needs schema tests and a preview tool for authors.

What we pick: Pick the versioned graph from the server. For a regulated product the question text is a legal artefact with an effective date, so it must be data that can exist in several versions at once, and the client must render the version the quote is pinned to. The cost is a rule language and a careful renderer, which the form-builder and config-driven-ui material already prepares.

The shape of the graph

A schema version holds sections, sections hold questions, and some questions are repeating groups: a template of sub-questions that is copied once per item. Every question has an optional showIf rule, and some carry flags that matter later: rating: true if it is a rating factor, knockout if certain answers end the journey, and a textVersion that changes whenever compliance edits the wording.

json
{
  "schemaId": "car-TX-2026-03-01",
  "sections": [{
    "id": "drivers",
    "questions": [
      { "id": "driverCount", "type": "integer", "min": 1, "max": 6, "rating": true },
      { "id": "drivers", "type": "repeat", "countFrom": "driverCount",
        "template": [
          { "id": "dob", "type": "date", "rating": true, "textVersion": 3 },
          { "id": "licenceYears", "type": "integer", "rating": true }
        ] },
      { "id": "priorClaims", "type": "boolean", "rating": true, "textVersion": 7 },
      { "id": "claims", "type": "table", "showIf": { "==": [{ "var": "priorClaims" }, true] } },
      { "id": "use", "type": "choice", "options": ["personal", "commute", "delivery"],
        "knockout": { "when": { "==": [{ "var": "use" }, "delivery"] }, "reason": "COMMERCIAL_USE" } }
    ]
  }]
}
Part of a car schema version. Rules are JSONLogic: plain JSON that both browser and server can evaluate.

JSONLogic is a small, documented format for rules written as JSON: {"==": [{"var": "priorClaims"}, true]} means "priorClaims equals true". It has libraries for JavaScript, Java, Python, PHP and more, so the browser and the server run the same rule file. It can't run arbitrary code, which is exactly what you want from rules that arrive over the network.

A custom rule language is the other choice. It can read better for authors, but you then own a parser in two languages. Start with JSONLogic, and add the few custom operations you need, like ageOn for age on the policy start date, through the library's extension point (add_operation in JavaScript). Each custom operation must then be written and tested on both sides.

How answers expand the question graph. The answer driverCount 2 repeats the driver template twice: Driver 1 and Driver 2, each with date of birth and licence. The answer priorClaims yes makes the claims table visible through its showIf rule. The answer use delivery triggers a knockout with reason COMMERCIAL_USE, which leads to alternatives such as a broker or a phone number. A note says the schema version is pinned to the quote and the rules are JSONLogic. The visible graph is a function of the schema and the answers, recomputed on every change.driverCountanswer: 2priorClaimsanswer: yesuseanswer: deliveryDriver 1dob, licenceDriver 2dob, licenceClaims tableshowIf: yesKnockoutCOMMERCIAL_USEAlternativesbroker, phonerepeat per driverschema versionpinned to quoterules: JSONLogicvisible graph = f(schema, answers), on every change
The schema never changes during a quote; the answers do. The visible graph is derived from both on every change, so it can't go stale.

Evaluating the graph on every answer

The client keeps the answers in one flat map keyed by path: drivers.0.dob, drivers.1.dob, priorClaims. After every change, a pure function walks the schema with the answers and returns the visible graph: the question instances shown right now. Repeating groups expand to as many copies as driverCount says, and questions whose showIf is false disappear.

For 200 questions this walk takes well under a millisecond, so there's no need to be clever. Recompute it from scratch each time, which also avoids a whole class of stale-state bugs.

What happens to answers that are no longer visible? If the customer says 3 drivers, fills all three, then changes to 2, the third driver's answers must not be rated or submitted. But throwing them away is unkind if the change was a slip.

So keep hidden answers in the draft for the session, exclude them from rating and from the submit payload, and drop them for good at submit. The server applies the same rule, so it never trusts a hidden answer.

Knockouts: ending the journey well

A knockout makes the customer ineligible for this product, such as commercial use of a private car, or a home that is a houseboat. The naive version shows a red error and a dead end. A good version stops the journey right away instead of after 40 more questions, and explains in plain words which answer caused it.

It also lets the customer go back and change that answer if it was a mistake, and offers alternatives such as a partner product, a broker, or a phone number. In the UK, for example, the FCA's Consumer Duty requires firms to avoid causing foreseeable harm to retail customers, and an unexplained dead end is an easy way to cause it.

The client evaluates the knockout rule for instant feedback, but the server decides. Some knockouts depend on data the client must never see, like an internal fraud check, so the server's answer can be a knockout the client didn't predict. The UI must handle a KNOCKOUT response at any time, with a reason code mapped to an approved message from the schema, never a raw server string.

Drivers
Prior claims
Vehicle use
Contact time

Change an answer. Rating answers start a re-quote; the contact time doesn't.

Question graph for 1 driver, no prior claims, personal use. Visible groups: About you, Car, Use, Driver 1, Cover, Review. Current revision 0. Price shown: none yet.visible groups (grey: hidden, green: just added)About youCarUseDriver 1Driver 2Driver 3Driver 4ClaimsCoverReviewabout 6 groupsrevision: 0price shown: nonecontact: morning, not ratedno rating requests yet
Change an answer. Rating answers start a re-quote; the contact time doesn't.

Pinning the version

When a quote starts, the server picks the schema version in effect for that product, market and date, and stores its id on the quote. The client fetches GET /schemas/car-TX-2026-03-01, which never changes, so it can be cached for a year with Cache-Control: public, max-age=31536000, immutable.

If the customer moves to another state, the server may need a new version. The client then receives a SCHEMA_SUPERSEDED response and migrates answers question by question, keeping only those whose id and type still match. It shows which answers need a fresh look; the disclosures chapter covers the same response for wording changes.

Check your understanding · Exercise 2

A customer changes the driver count from 3 to 2 after filling in all three drivers. Which behaviours are correct? Select all that apply.

Live pricing: re-quoting without lying

Once the customer has answered enough rating factors, the price panel shows a premium, and it should follow their answers. The naive approach calls the rating engine on every change, so typing a 7-character postcode fires 7 requests. A rating call often takes 300 ms to 2 seconds, because it may call reference data, a credit check, and a pricing model.

Responses then arrive out of order, so the price for "SW1A 1" can land after the price for "SW1A 1AA" and overwrite it. The customer sees £84, then £91, then £84 again, and screenshots the wrong one.

The opposite approach, pricing only at the end, is simple. But people abandon long forms when they can't see whether the price is worth the effort, so we need something in between.

Decision When should the client ask the rating engine for a new price?

On every change

Any answer change sends POST /applications/:id/rate straight away.

Pros

  • The price is always as fresh as possible.

Cons

  • Floods the rating engine, which is often billed per call or rate limited.
  • Out-of-order responses show wrong prices.
  • The price flickers while typing.

On section continue

Rate when the customer presses Continue at the end of a section.

Pros

  • Few calls, simple, no races within a section.

Cons

  • Changing an earlier answer from the review page doesn't re-price until another Continue.
  • Long sections feel unresponsive.

On committed rating answers

Only answers flagged rating: true in the schema trigger a re-quote.

Trigger on commit (blur, select, or Continue), debounce by 600 ms, cancel the previous request, and accept only the latest revision.

Pros

  • About one call per material change, the price follows the answers, and stale prices can't win.

Cons

  • More client logic: a request state machine with revisions and cancellation.
  • Needs the schema to mark rating factors correctly.

What we pick: Re-quote on committed rating answers. The schema already knows which questions are rating factors, so a change to "preferred contact time" costs nothing, while a change to the car model re-prices within a second. Debounce plus cancellation plus a revision number turns a race-prone call into a predictable one; request-races and debounce-throttle cover those tools in depth.

State machine of the quote. Not priced moves to Recalculating when enough rating factors are answered. Recalculating moves to Priced when a reply arrives with the current revision, and Priced moves back to Recalculating when a rating answer is committed. Recalculating moves to Error when the call fails, and Error returns to Recalculating on retry or a new answer. Priced moves to Expired when the expiry passes, and Expired goes back to Recalculating on resume. A knockout reply moves to Knocked out. A newer answer while recalculating aborts the old call and starts again.Not pricedtoo few factorsRecalculatingold price dimPricedrev = currentKnocked outreason codeErrorkeep last priceExpiredre-rate on resumeenough factorsreply, same revnew answerfailsretryexpiresresumeknockoutnewer answer here:abort, start againa reply is applied only if its revision is the current one
Every arrow into Priced checks the revision: a reply for an older revision changes nothing. Error and Recalculating keep the last price on screen, dimmed, so the layout never jumps.

Revisions beat timestamps

Every committed answer change increments an answers revision on the client. A rating request carries that revision, the server echoes it back with the price, and the client applies a price only if its revision equals the current one. The AbortController stops wasted work in the browser, but it can't stop a response that has already arrived, and on a slow connection the abort and the response can cross. The revision check is what makes the race impossible, not the abort.

ts
let revision = 0;
let inflight: AbortController | null = null;
let timer: ReturnType<typeof setTimeout> | undefined;

export function onRatingAnswerCommitted(appId: string) {
  revision += 1;
  const mine = revision;
  quoteStore.set({ status: "recalculating", revision: mine });
  clearTimeout(timer);
  timer = setTimeout(async () => {
    inflight?.abort();
    inflight = new AbortController();
    try {
      await saveQueue.flush(); // the server rates what it has recorded
      if (mine !== revision) return;
      const res = await api.rate(appId, { revision: mine }, inflight.signal);
      if (mine !== revision) return;
      quoteStore.set({ status: "priced", ...res });
    } catch (error) {
      if (mine !== revision || isAbort(error)) return;
      quoteStore.set({ status: "error", revision: mine, error: toQuoteError(error) });
    }
  }, 600);
}
The core of the re-quote loop. The client only decides when to ask and which reply to keep.
Sequence of two quick rating answers. The customer commits car Golf; the form tells the quote controller revision 4, which calls the rating API for revision 4. The customer then commits driver age 19; the form sends revision 5, the controller aborts revision 4 and calls rate for revision 5. The reply for revision 4, 84 pounds, arrives late and is dropped. The reply for revision 5, 131 pounds, is applied, and the form shows 131 pounds with the reason. If the call fails, the last price stays dimmed with a retry.CustomerFormQuote ctlRating APIcar: Golfrev 4rate rev 4driver age 19rev 5, abort 4rate rev 5late: £84, rev 4£131, rev 5£131 + whyrev 4 dropped (4 is not 5); on error: keep £84 dimmed, offer retry
The late reply for revision 4 is dropped because 4 is not the current revision, even if abort() came too late to stop it. On a failure, the old price stays dimmed and a Retry appears; the customer can keep answering.

Showing "recalculating" without jumping

While a new price is on its way, keep the old number visible but dimmed, with a small "Updating price" label in a fixed-size slot, so nothing in the layout moves. Removing the number and showing a spinner makes the panel shrink and grow, which is distracting while typing. A shift that lands more than half a second after the customer's input also counts toward Cumulative Layout Shift (CLS).

Put the price in an aria-live="polite" region and keep the "Updating" label outside it. Screen reader users then hear "New price: £91 a month" once, instead of three announcements.

Keep the Continue button enabled while re-pricing. The customer is answering questions, not paying, and blocking them on the rating engine makes a 2-second wait feel like a 20-second one. The one place to wait for a fresh price is the final review before signing, where the price shown must match the price bound.

Explaining big price changes

A jump from £84 to £131 after adding a 19-year-old driver is correct, but without a reason it looks like a trick. Ask the rating API to return the main factors behind a change, as reason codes the schema maps to approved text: "Adding a driver under 25 increased your price". The client shows this note when the change exceeds a threshold, say 10%, and never invents reasons itself. Some markets restrict what can be said about rating factors, so the wording is owned by compliance, just like question text.

Quote reference and expiry

Each successful rating returns a quote reference and an expiry. The reference is what the customer quotes on the phone, so show it in the price panel and in emails. The expiry is a product rule: many car and home quotes are valid for around 30 days, but it varies by insurer and market (treat 30 as an estimate).

The price is valid only for the answers and the pricing model version it was computed with. After expiry, or after the insurer publishes new rates, resuming the quote must re-rate, and the UI must say so plainly: "Prices have been updated since your last visit." Never show an expired price as if it were current.

Check your understanding · Exercise 3

A slow rating response for revision 4 arrives after the response for revision 5 has already been shown. What should keep the screen correct? Pick one.

Prefill and verification: fewer questions, same honesty

The fastest question is the one you don't ask. On UK sites such as Admiral or Compare the Market, a registration like AB12 CDE fills in the make, model, engine size and year. In the US, a VIN lookup does the same, and in India the registration number can return the vehicle details from the national registry.

Address lookup fills the property, and some insurers check claims databases: CLUE reports in the US, the Claims and Underwriting Exchange (CUE) in the UK. Where the law allows, a credit-based insurance score is pulled too; several US states ban or limit it, for example California, Hawaii, Massachusetts and Michigan for car insurance. Which sources exist is country specific, so the schema, not the client, says which lookups a question can use.

The naive approach fills fields silently and moves on. That breaks the contract: the answer on the application is now something the customer never saw, and if the lookup was wrong (a car with a modified engine, an address with a granny flat), the customer has made a misrepresentation without knowing. It also breaks trust when a third-party check happens without asking.

Decision How should prefilled values become answers?

Silent fill

Lookup results are written straight into the answers.

Pros

  • Fewest clicks, highest completion rate in the short run.

Cons

  • The customer never confirmed the value, so it is weak evidence later.
  • Wrong data goes unnoticed until a claim.

Fill and confirm

Lookup results are shown as a summary card: "We found: Ford Fiesta 1.0 EcoBoost, 2019. Is this right?"

Confirm accepts all; Change opens the fields to edit. The source of each value is recorded.

Pros

  • Still fast: one click confirms 6 values.
  • The customer saw and accepted each value, which is what the contract needs.
  • Fits the spirit of WCAG 3.3.7 Redundant Entry: show what we already know instead of asking for it again.

Cons

  • One extra screen per lookup.
  • Needs a clear Change path that doesn't lose the other prefilled values.

Ask, then verify

The customer answers every question, and the server checks the answers against databases afterwards.

Pros

  • The customer's own words are on record.

Cons

  • Slow, and asks people things they don't know by heart, like engine size.
  • Mismatches surface late, sometimes after the price was shown.

What we pick: Fill and confirm, with verification on top. Prefill turns 6 hard questions into one confirmation, and the confirmation is what turns lookup data into the customer's answer. For answers that are typed rather than looked up, such as claims history, the server still checks them against databases and the client flags mismatches for a decision.

Sequence of a prefill. The customer acknowledges the data checks notice; the app sends the consent acknowledgement, version 2, to the BFF. The customer types registration AB12CDE; the app asks the BFF to look up the vehicle, and the BFF queries the vehicle registry, which returns Fiesta 1.0, 2019. The BFF returns the values with their source; the app asks: is this your car? The customer presses Change to correct the engine. If no consent was recorded, the BFF answers 403 CONSENT_REQUIRED and the app shows the notice.CustomerAppBFFRegistryack checksconsent v2reg AB12CDElookup vehiclequeryFiesta 1.0, 2019values + sourceis this your car?change engineno consent recorded: 403 CONSENT_REQUIRED, show the notice
The lookup is gated by a recorded consent, and its result is a suggestion until the customer confirms it. Each value keeps its source: prefill-confirmed or prefill-corrected.

Some lookups need consent first. A credit check, a claims database search, or a driving licence check usually requires a notice that the check will happen and why, and in some markets explicit consent. The schema marks these lookups with a consentId, and the client shows that notice and records its acknowledgement before calling the lookup endpoint.

The server enforces it too. Here that is the backend-for-frontend (BFF), a thin server layer that sits between the browser and the insurer's systems: a lookup request without a recorded consent for that quote is rejected with CONSENT_REQUIRED. The client never decides on its own that consent is not needed.

Mismatches

A mismatch is when the customer's answer and a database disagree: they said no claims in 5 years, and the claims database shows one in 2023. The client must not call the customer a liar or silently overwrite their answer. It shows what was found, neutrally, with choices: "We found a claim on 14 June 2023. Add it to your claims, or tell us why it doesn't apply."

Both choices are recorded. Some mismatches go to a human underwriter instead, and the client shows a "We need to check a few details" state with a clear next step.

Every answer remembers its source: typed by the customer or agent, or prefilled from a named lookup at a known time. Prefilled values the customer accepted become prefill-confirmed, and edited ones prefill-corrected. This field feeds the audit trail in the next chapter, and it lets the review page show a small "From vehicle records, confirmed by you" note next to the value.

Check your understanding · Exercise 4

A registration lookup returns the car's make, model, fuel, engine size and year. What should the client do? Select all that apply.

Disclosures, consent and the audit trail

Two things make an application legally solid: proof of what the customer was told, and proof of what they said. The first is disclosures: required statements such as "Your answers must be true and complete, or your policy may be cancelled or a claim refused", the key facts of the cover, and data use notices. The second is the audit trail: the history of every answer.

Wrong or missing answers can affect claims later, which is why the insurer must be able to show exactly what was asked and what was said. This is not legal advice; the point for the frontend is what must be recorded.

Disclosures as versioned blocks

A disclosure is a schema node like a question, with an id, a textVersion and a content hash. Some must only be shown, some must be acknowledged with a checkbox or button before Continue is allowed, and some must be opened as a document.

The client sends each acknowledgement as { disclosureId, textVersion, textHash, shownAt } with the next save, and the server stamps the actor and the time, as it does for answers. Recording the hash of the exact text rendered, not just the id, means a later edit to the disclosure can't silently change what the customer appears to have agreed to.

Don't pre-tick consent boxes, don't bundle unrelated consents (marketing with the declaration), and don't hide the text behind a link that no one opens when the regulator expects it to be shown. Accessible acknowledgement is a real input type="checkbox" with a label, and an error from the error summary if they press Continue without it.

Decision How do we keep a trustworthy history of answers?

Latest answers only

The draft stores the current value of each answer; history lives nowhere.

Pros

  • Simplest storage and API.

Cons

  • Can't show who changed an answer or what it was when the price was quoted.
  • Fails the first claims dispute that asks "did the agent change this?"

Client-built log

The browser keeps a change log and uploads it at submit.

Pros

  • Captures every keystroke-level change.

Cons

  • A log built on a device you don't control isn't evidence; it can be edited or lost with the tab.
  • Huge and noisy.

Server-stamped answer events

Each save sends committed changes as events with the question id, path, value, and textVersion the client rendered.

The server stamps time and actor from the session, appends them to an append-only log, and derives the current answers from it.

Pros

  • The actor and time come from the server, so they can't be faked by the client.
  • Current answers, history, and any past point in time all come from one log.

Cons

  • More storage, and the API must accept batches and be idempotent.
  • Needs care with personally identifiable information (PII): the log holds the same sensitive values as the answers.

What we pick: Server-stamped answer events. The client knows which wording it rendered, so it sends the question id and text version; the server knows who is signed in and what time it is, so it stamps actor and time. Neither side alone can produce a trustworthy record. Events are recorded on commit (blur, select, Continue), not per keystroke, which keeps an application around a few hundred events.

ts
type AnswerEvent = {
  clientEventId: string;     // uuid, makes retries idempotent
  path: string;              // "drivers.1.licenceYears"
  value: AnswerValue | null; // null when cleared
  questionTextVersion: number;
  source: "typed" | "prefill-confirmed" | "prefill-corrected";
  // stamped by the server:
  seq: number;
  at: string;                // ISO time from the server clock
  actor: { kind: "customer" | "agent"; id: string };
  schemaId: string;          // "car-TX-2026-03-01"
};
One answer event. The client sends the first five fields; the server adds the rest.
The audit trail. The customer app and the agent console both send answer events with path, value and the question text version they rendered. The BFF stamps each event with the actor and the server time and appends it to an append-only event log. Current answers are derived from the log. On submit, the BFF writes a hashed, frozen snapshot. An example event: drivers.1.licenceYears equals 3, text version 3, agent A-17, at 10:42:07.customeragentCustomer apppath, value, text vAgent consolesame formatBFFstamps actor, timeEvent logappend onlyAnswersderivedSnapshothashed, frozenon submitdrivers.1.licenceYears = 3text v3, agent A-17, 10:42:07events, not overwrites: any past moment can be rebuilt
The client knows the wording it rendered; the server knows who and when. Together they make an event worth keeping, and the snapshot is built from the log, never from the browser's copy.

Check answers, sign, and freeze

Before binding, the customer sees a check your answers page, as in the GOV.UK Design System pattern: every answer grouped by section, each with a Change link that goes to that question and comes back. This is also how we meet WCAG 3.3.4 Error Prevention (Legal, Financial, Data), which asks that submissions with legal or financial effect are reversible, checked, or confirmed. Then come the declaration, the e-signature (typed name, a checkbox and a click, or a drawn signature, depending on product and market), and submit.

On submit, the server builds the application snapshot: the schema id, the visible questions with their exact text versions, every answer and its source, every disclosure acknowledgement, the price and quote reference, and the signature. It stores it as an immutable, hashed document and returns its id. From then on, any change is a mid-term adjustment on the policy, with its own record, never an edit to the snapshot.

The confirmation page states the cancellation right in plain words, with the period set per market in the schema. In the UK, the FCA's ICOBS rules give consumers 14 days for most general insurance like car and home, and 30 days for life and other pure protection cover. India's IRDAI has set a 30-day free-look period for most new life and health policies since 2024, and US rules vary by state.

When the rules change mid-application

The quote is pinned to its schema version, so usually nothing changes. But some changes can't wait: a disclosure correction ordered by the regulator, or a quote resumed after its expiry when the old version is no longer allowed. Then the server answers the next save with SCHEMA_SUPERSEDED and the new schema id.

The client keeps answers whose question id, type and text version are unchanged. Questions with new wording need a fresh answer, because the old answer was given to different words, and any new disclosure must be acknowledged again. The audit log keeps both versions, so the history shows exactly when and why the customer was asked again.

Typing
Years licensed
Prior claims (v7)

Pick who is typing, change an answer, then submit.

Audit log with 0 events. Not submitted yet.audit log, stamped by the server (last 5)no events yet: change an answernot submitted: answers can still changeschema car-TX-2026-03-01, claims wording v7
Pick who is typing, change an answer, then submit.

Check your understanding · Exercise 5

Which fields of an answer event should the server set rather than trust from the client? Select all that apply.

Agent-assisted applications and the hand-off

Many customers get stuck, call, and finish with an agent: a call-centre employee or an independent broker. The agent can't simply take the customer's password and fill in the form; the record would then say the customer typed answers the agent typed. And the agent can't sign the declaration for the customer: in most markets the customer must attest that the answers are true, often in their own words or with their own click.

Three approaches are common, and an insurer often supports more than one.

Decision How should an agent work on a customer's application?

Co-browsing

The customer stays on their own screen; the agent sees it through a co-browsing tool and guides them, maybe with a pointer.

Sensitive fields are masked in the agent's view.

Pros

  • Every answer is typed by the customer; the record stays simple.
  • Good for confident customers who just have a question.

Cons

  • Needs a co-browsing vendor or a custom DOM-mirroring channel, with masking rules per field.
  • Doesn't help customers who can't use the form at all.

Agent console with hand-off

The agent opens the same application in their own console, signed in as an agent, and answers while asking the questions out loud.

When ready, the agent sends the customer a one-time link to review the answers on their own device and attest.

Pros

  • Works for anyone with a phone; the agent's tools (scripts, notes) live in the console.
  • Clear record: agent answers are stamped as agent, attestation is stamped as customer.

Cons

  • Two devices on one application need a clear rule for who is editing.
  • The customer must re-read every answer, which some skip.

Shared login

The agent signs in as the customer.

Pros

  • No extra work.

Cons

  • Destroys the audit trail and breaks most insurers' and regulators' rules. Never do this.

What we pick: Support the agent console with hand-off as the main path, and co-browsing as an option for customers who want to drive. Both keep the actor honest in the audit trail. The shared login is listed only so you can reject it out loud in the interview.

Sequence of an agent hand-off. The agent sends answers stamped as agent, and the server confirms the agent holds the edit lease. The agent presses hand off, and the server sends the customer a one-time link by SMS. The customer opens it and verifies a code; the server shows the review and gives the lease to the customer, while the agent's view becomes read-only. The customer fixes one answer and signs, and the server tells the agent it was signed by the customer. If the link expired or the code is wrong, no answers are shown.AgentServerCustomeranswers (actor: agent)lease: agenthand offSMS: one-time linkopen + verify codereview, lease: youread-only viewfix 1 answer, signsigned by customerlink expired or code wrong: show nothing, agent resends
The edit lease moves with the hand-off, so only one device can change answers at a time. Only the customer's device ever gets a Sign button.

One editor at a time

When the customer opens the link while the agent still has the application open, two devices can edit. Merging answer by answer is possible (the conflict-resolution lesson covers it), but for a legal record a simpler rule is better: an edit lease. The device holding the lease can edit; the other shows the application read-only with "Being edited by your agent" or "The customer is reviewing".

The lease is a server record with an expiry of a few minutes, renewed by the editing device every 30 seconds and released on hand-off. Taking over is an explicit action, recorded in the log.

Saves include the last event seq the client saw. If another device wrote since then, the server returns 409 Conflict with the new events, and the client shows what changed instead of overwriting it. With the lease this should be rare, but it protects against a lease that expired while a laptop slept.

Who may do what

ActionCustomerAgentWhy
Answer questionsYesYes, stamped as agentBoth are normal; the record says who
See full national id or health detailsYes, after re-authMasked unless their role allowsLeast privilege on PII
Acknowledge customer disclosuresYesNo, only record that they were read aloudThe customer must receive them
Sign the declarationYesNoAttestation is personal
Override a knockoutNoOnly underwriters, with a reasonA recorded human decision

The client renders these rules from the permissions the server returns with the application, as in the permission-based-ui guide: the Sign button simply doesn't exist in the agent console. The server enforces the same rules, because a hidden button is not a security control.

Check your understanding · Exercise 6

An agent fills a whole application on the phone and the customer opens the review link. Which design choices are right? Select all that apply.

Sessions, resume links and sensitive data

The multi-step-form guide already saves a server draft and resumes it on another device. Two things are different here: the draft holds health conditions, driving offences and national ids, and the insurer's security team wants short sessions, often 15 to 30 minutes of inactivity. Short sessions and a 200-question form are in direct conflict. The naive version logs the customer out mid-section, drops what they typed, and lands them on a sign-in page.

WCAG 2.2 sets the floor. 2.2.1 Timing Adjustable (Level A) requires that the user can turn off or adjust a time limit, or is warned before it expires and given at least 20 seconds to extend it with a simple action, at least ten times. 2.2.5 Re-authenticating (AAA) asks that after re-authenticating, the user can continue without losing data.

2.2.6 Timeouts (AAA) asks that users are warned how long inactivity can last before data is lost, unless the data is kept for more than 20 hours. A server draft that lasts for days meets it, and saying the session length once at the start is still kind. An insurer can meet all three, and should.

Decision How should the application handle an idle session?

Hard timeout

After 20 idle minutes the session ends and the page redirects to sign in.

Pros

  • Simplest, and gives the security team its short session.

Cons

  • Fails WCAG 2.2.1 without a warning.
  • Loses unsaved answers and drives abandonment.

Warn, extend, and save

Answers are saved on commit, so nothing is lost at expiry.

Two minutes before expiry a dialog says how long is left with an Extend button; at expiry, PII is cleared from the screen and a sign-in returns to the same question.

Pros

  • Meets 2.2.1, 2.2.5 and 2.2.6, and keeps the security posture.
  • Shared or public devices don't leave health data on screen.

Cons

  • Needs a dialog that works with screen readers and doesn't steal focus too early.
  • Cross-tab coordination so one active tab keeps the session alive.

What we pick: Warn, extend, and save. Committed answers are already on the server, so the timeout only protects the screen, not the data. The dialog is a modal with focus moved to it (focus-management covers the rules), a visible countdown that announces only at a few points, not every second, and a single Extend button. A BroadcastChannel shares activity between tabs so a customer working in one tab isn't warned in another.

Timeline of an idle session. At 0 minutes the last answer is saved. At 13 minutes a warning dialog appears with Extend and 2 minutes left; pressing Extend resets the timer to 0. At 15 minutes the session expires and personal data is cleared from the screen. Signing in again returns to the same question. WCAG 2.2.1 requires a warning, at least 20 seconds to extend, and at least 10 extensions.0 min: last activityanswer savedWarning dialogExtend, 2:00 leftExtend resets13 min15 minExpiredPII clearedsign in again:back to same questionWCAG 2.2.1: warn, 20+ seconds, extend 10+ times
Nothing is lost at expiry because every committed answer is already on the server; the timeout only protects the screen. Extend is one button press, as WCAG 2.2.1 asks.

"We've emailed you a link to continue" is a big conversion win, and a big risk: email is forwarded, inboxes are shared, and links leak into proxy logs. The link carries a random, single-use token that expires (7 days is a common choice; match it to the quote expiry), never the quote id alone and never any personal data.

Opening it lands on a page that shows only the product and the quote reference. It then asks the customer to prove who they are, with a one-time code or date of birth plus postcode, before any personal data is shown. That is re-authentication, and the auth-flow and otp-input guides cover the pieces.

Rules for sensitive data

  • Nothing personal in URLs. The route is /quote/Q-8F3K/drivers/2, never a name, date of birth or registration in the path or query. URLs end up in history, referrers, analytics and server logs.
  • Analytics and logs get ids, not values. Track question_answered with the question id and section, never the answer. Error tracking scrubs request bodies and form values before sending.
  • Field-level encryption for the worst fields. National ids, health answers and offences are encrypted by the server with a separate key, and the client receives them masked (•••• •••• 4821) unless the user re-authenticates and asks to see them.
  • No PII in long-lived browser storage. Drafts live on the server. If you keep a local copy for resilience, keep it in memory or sessionStorage, never localStorage, and clear it on sign-out, submit and timeout.
  • Correct autocomplete tokens where they exist: given-name, family-name, bday, email, tel, postal-code, address-line1. They save typing and help people with motor and memory impairments. There is no standard token for national ids or health details, so leave those to the user.

Check your understanding · Exercise 7

A customer is idle on the medical history section of a life application. Which behaviours are right? Select all that apply.

Architecture, data model and API

With the hard parts decided, the parts of the app follow from one rule: the client owns rendering, input and timing; the server owns truth, which means the schema versions, prices, the audit log, permissions, and the snapshot. Every boundary below follows from that rule.

Architecture. In the browser: the Section renderer writes committed answers to the Answer store, which feeds the Rule engine, the Quote controller and the Save queue. The Quote controller updates the Price panel. The Session guard watches idle time and clears personal data. The Schema cache holds immutable schema versions. On the server: the BFF adds the actor and permissions, calls the Rating engine, and appends to the Event log; the Schema store serves versions to the schema cache.BrowserSection renderercurrent groupAnswer storeby path, revisionRule enginevisible graph outSession guardidle, PII clearPrice panelaria-live politeQuote ctldebounce, revisionSave queueevents, retrySchema cacheimmutable by idServerRating engineprice, reasonsBFFactor, permissionsEvent logappend onlySchema storeversions
The renderer reads the visible graph and writes answers; it never calls the server. Network calls live in the quote controller, the save queue and the schema cache (plus the lookup client for prefill, not drawn), so side effects stay in a few small, testable places.
PartResponsibilityWhy it's separate
Schema loaderFetches the pinned schema version by id, caches it as immutableSchema changes rarely and is large; cache it apart from answers
Rule engineEvaluates JSONLogic rules: visibility, repeats, knockouts, validationPure and shared with the server, so it's testable alone
Answer storeAnswers by path, revision counter, dirty set of uncommitted fieldsOne source for the renderer, the save queue and the quote controller
Section rendererRenders the current section from the visible graph with the question type registryGeneric; the same code serves car, home and life
Save queueBatches committed answer events, retries with the same clientEventId, handles 409Saving must not block typing, and retries must not double-record
Quote controllerDebounces rating answers, cancels stale calls, holds the price state machinePricing has its own timing and failure modes
Session guardIdle timer, warning dialog, tab coordination, PII clearingSecurity behaviour in one auditable place
Lookup clientPrefill calls, consent checks, mismatch resultsEach lookup is gated by consent and is market specific

The section renderer never talks to the network: it reads the visible graph and the answers, and writes changes to the answer store. The store then notifies three listeners: the save queue (always, on commit), the quote controller (only if the changed question has rating: true), and the rule engine, which recomputes the visible graph. This one-way flow keeps the renderer simple and makes the expensive side effects explicit.

The agent console is the same app with a different shell. It adds a call script panel, notes, and the hand-off button, and it receives different permissions from the server. Building it as a separate app would mean two renderers for one legal document, which is how wording drifts between channels.

Data flow for one answer. 1: the customer picks Yes for prior claims, and the renderer sets it in the store. 2: the rule engine reads the answers and returns a visible graph with the claims table, which the renderer shows. 3: on blur the save queue sends the answer event to the BFF, which appends it to the event log. 4: because prior claims is a rating factor, the quote controller waits 600 milliseconds and asks the BFF to rate revision 12; the rating engine replies with a price, which goes to the price panel.customerRendererStoreRule engineSave queueQuote ctlEvent logvia BFFRatingvia BFFPrice panel12341 answer set2 claims shown3 event saved4 rate rev 12
Each arrow carries something small: an answer, a graph, an event, a revision number. The price comes back by the same path and lands only in the price panel.

The data flow figure follows one answer, "Yes" to prior claims, from the renderer to the price panel. The claims table appears at once because the visible graph is derived locally. The answer event is saved on blur, and because prior claims is a rating factor, the re-quote for revision 12 follows 600 ms later. Only two requests leave the browser, and the customer is never blocked.

Check your understanding · Exercise 8

Which parts should be shared between the customer app and the agent console? Select all that apply.

Data model and API

Four kinds of data live in the client, and each has a different home. The schema version is immutable, so it sits in the HTTP cache and in a memory cache keyed by schema id. The application (answers, acknowledgements, permissions, lease) is server state, fetched once on load and then updated by the client's own saves. The quote (price, reference, expiry, status) has its own state machine in the quote controller.

UI state such as the open section and the timeout dialog is component state, and the current section is also in the URL, without personal data.

The client data model in four homes. In the HTTP cache: SchemaVersion, immutable and keyed by schemaId, with Cache-Control immutable. In server state: Application, with answers, acknowledgements, lease and permissions, and lastSeq 42. In the quote controller: Quote, with status, revision and price, and expiresAt. Derived and never stored: the visible graph, computed from the schema and the answers on every change.HTTP cacheSchemaVersionimmutable, by schemaIdCache-Control: immutableServer stateApplicationanswers, acks, lease, canlastSeq: 42Quote controllerQuotestatus, revision, priceexpiresAtDerivedVisible graphf(schema, answers)recomputed, never stored
The visible graph has no home of its own: it is a function of the schema and the answers. Storing it would only create a copy that can disagree with them.
ts
type Application = {
  id: string;                      // "APP-71C2"
  quoteRef: string;                // "Q-8F3K", shown to the customer
  schemaId: string;                // pinned: "car-TX-2026-03-01"
  answers: Record<string, AnswerValue>;   // "drivers.0.dob" -> "1990-04-02"
  sources: Record<string, AnswerSource>;  // "vehicle.model" -> prefill-confirmed
  acks: DisclosureAck[];
  lastSeq: number;                 // last audit event seen
  lease: { holder: "me" | "other"; until: string } | null;
  can: { edit: boolean; sign: boolean; seeUnmasked: boolean };
  status: "draft" | "referred" | "knocked-out" | "submitted";
};

type Quote =
  | { status: "idle" | "recalculating"; revision: number; last?: Price }
  | { status: "priced"; revision: number; price: Price; expiresAt: string }
  | { status: "error"; revision: number; error: QuoteError; last?: Price };

type Price = { monthly: number; annual: number; currency: string; reasons: string[] };
Client-side types. Answers are a flat map by path; everything that matters legally carries a version.

Note what is missing: the client never stores the visible graph. It is always derived from the schema and the answers, so it can't go stale. Also missing: the audit log. The client sends events but never needs the whole history, except in the agent console's history panel, which fetches it on demand.

Endpoints

http
GET  /schemas/car-TX-2026-03-01            -> 200 schema (immutable, cached 1 year)
GET  /applications/APP-71C2                -> 200 Application
POST /applications/APP-71C2/events
     { baseSeq: 40, events: [{ clientEventId, path, value, questionTextVersion, source }],
       acks: [{ disclosureId, textVersion, textHash, shownAt }] }
     -> 200 { lastSeq: 42, knockout?: { reason: "COMMERCIAL_USE" } }
     -> 409 { code: "STALE_BASE", events: [...] }
     -> 409 { code: "SCHEMA_SUPERSEDED", schemaId: "car-TX-2026-04-01" }
POST /applications/APP-71C2/rate   { revision: 12 }
     -> 200 { revision: 12, price, quoteRef, expiresAt, reasons: ["YOUNG_DRIVER_ADDED"] }
     -> 422 { code: "MISSING_RATING_FACTORS", paths: ["drivers.1.dob"] }
POST /applications/APP-71C2/lookups/vehicle  { registration: "AB12CDE" }
     -> 200 { values: {...}, source: "vehicle-registry" } | 403 { code: "CONSENT_REQUIRED" }
POST /applications/APP-71C2/lease          -> 200 { until } | 423 { holder: "agent" }
POST /applications/APP-71C2/submit  { signature, revision: 12 }
     -> 201 { snapshotId, policyNumber } | 409 { code: "PRICE_CHANGED", price }
The main calls. All go through the BFF, which adds the actor from the session.

Two details matter. First, the rate call sends only the revision, not the answers: the server rates from the log it already holds, which proves the price matches the recorded answers. So the save for revision 12 must land before the rate for revision 12, and the quote controller waits for the save queue to flush first.

Second, submit carries the revision the customer saw the price for; if the price changed in between, the server refuses with PRICE_CHANGED and the client shows the new price for confirmation instead of binding silently. Error codes follow the api-contracts-and-errors lesson: a stable code the client maps to approved text, never a server message shown raw.

Check your understanding · Exercise 9

Why does the rate endpoint take a revision instead of the full answers? Select all that apply.

Accessibility and performance of a 200-question form

A long legal form is where accessibility failures hurt most: someone who can't complete it can't buy insurance, and someone who misunderstands a question may lose a claim. The semantic-html, aria-patterns, focus-management and screen-readers lessons cover the basics. This chapter covers what is specific to very long, branching forms.

Decision How many questions should one page hold?

One thing per page

The GOV.UK Design System pattern: each page asks one question or one tightly related group, with the question as the page heading.

Pros

  • Easy to understand, easy to validate, works well on phones and with screen readers.
  • Errors are on one small page; branching is natural.

Cons

  • 200 questions become well over 100 pages; repeating groups get tedious.
  • More round trips if each page saves and validates on the server.

Long sections

One page per section (Car, Drivers, History) with 10 to 30 questions.

Pros

  • Fewer page turns, easy to scan and compare answers in a section.

Cons

  • Errors spread down a long page; questions that appear above the focus are easy to miss on a screen reader.
  • Heavy pages on low-end phones.

Small groups

One page per group of 1 to 6 related questions in a fieldset with a legend, such as "Driver 2: licence".

Simple single questions get their own page.

Pros

  • Most of the clarity of one thing per page with a third of the pages.
  • Groups map directly to schema sections and repeat items.

Cons

  • Authors must design groups well; the schema needs a grouping field.

What we pick: Small groups, with the GOV.UK one-thing-per-page rule applied to any hard or sensitive question (a knockout, a health condition, the declaration). The grouping lives in the schema, so compliance and design can tune it without code, and analytics shows which groups people abandon.

Errors, help and plain language

  • Error summary at the top. On Continue with errors, show a summary box with a heading and one link per error, move focus to it, and prefix the page title with "Error:". Each link jumps to its field. This is the GOV.UK error summary pattern, and with the inline errors below it meets WCAG 3.3.1 Error Identification.
  • Inline errors tied to fields with aria-describedby, and aria-invalid="true" on the field, in words that say how to fix it: "Enter the year the claim happened, like 2023".
  • Grouped questions in a fieldset whose legend is the question, so radio groups and date parts are announced with their question.
  • Help text that is visible, not hidden in a tooltip: "A claim is any time you asked an insurer to pay, even if they didn't." Definitions matter because a misunderstood term can easily turn into a misrepresentation.
  • Plain language owned with compliance: the filed wording can still be short sentences. Test reading level, and offer a plain explanation below legal text where the legal text can't change.

Progress that doesn't lie

A percentage bar jumps backwards when a "Yes" adds a claims table, and customers notice. Show sections with a status (done, current, not started) instead of a percentage, and inside the current section say "Question group 3 of about 5". The word "about" is honest: the estimate comes from the visible graph plus the schema's typical size for unanswered branches. When an answer adds questions, announce it in a polite live region: "2 questions added about your claims." Try it in the question graph lab above.

Performance

A 200-question schema with wording, help text and rules is roughly 80 to 150 KB of JSON, and 15 to 30 KB compressed (an estimate; measure yours). Load it once, cache it forever by schema id, and parse it once into a lookup by question id. Render only the current group, never all 200 questions hidden with CSS: hidden inputs still cost DOM nodes, memory and validation time on low-end phones.

Rare, heavy question types, such as a medical condition picker with 5,000 coded terms or a claims table, are code-split and loaded when their branch becomes visible, with a prefetch as soon as a parent answer makes them likely. Keep each input change under 100 ms of work so Interaction to Next Paint (INP) stays within the 200 ms "good" threshold. The rule engine walk is cheap, but re-rendering every question on every keystroke is not, so subscribe each field to its own path in the store.

Server validation errors come back as paths, so the error summary links to them just like client errors. Test with a keyboard only, with NVDA and VoiceOver, and at 400% zoom, on the longest real branch, not the happy path; automated checkers find only part of the problems.

Check your understanding · Exercise 10

A customer presses Continue on a group with two errors. What should happen? Select all that apply.

Running it in the interview

In 60 minutes you can't cover every chapter in depth, so choose. Spend about 8 minutes on requirements, and name the five properties from the requirements chapter: graph, legal wording, live price, contract, several actors. Then draw the wireframe and the architecture in 10 minutes. Spend the next 25 minutes on two or three deep dives, chosen by what the interviewer reacts to, and keep 10 minutes for follow-ups and wrap-up.

Draw the question graph first: a section with a repeating group, a conditional table, and a knockout. It makes "questions are versioned data" obvious without a long explanation. Then draw the quote controller with its revision number; interviewers often probe races, and a revision number on the board answers the question before it is asked.

Say "the server owns truth: schema, price, log, permissions" early, and repeat it whenever you make a boundary decision.

Follow-ups to expect

Think first

Compliance changes the wording of a question today. What happens to a customer halfway through their application?

Think first

The customer reviewed a price of £91, then the insurer published new rates before they signed. What should the submit do?

Think first

Should the application work offline?

Check your understanding · Exercise 11

You have 10 minutes left and haven't covered the audit trail. Which summary is strongest? Pick one.

Summary

  • Model the questions as a versioned graph from the server: sections, repeating groups, conditions and knockouts in JSONLogic, pinned to the version the quote started on.
  • The schema marks rating factors; only committed changes to them re-price, debounced, with stale requests cancelled and a revision check so an old price can never win.
  • Prefill turns lookups into one confirmation per card, after consent; every answer remembers its source, and mismatches are shown neutrally with a choice.
  • Record disclosures by text version and hash, and answers as server-stamped events with the rendered wording; freeze a hashed snapshot on submit.
  • Agents work in their own console with their own identity; an edit lease and a hand-off link let the customer review and sign on their own device.
  • Timeouts warn and extend (WCAG 2.2.1), committed answers are already saved, resume links expire and require re-authentication, and PII stays out of URLs, analytics, logs and long-lived storage.
  • For accessibility, use small groups in fieldsets, an error summary that links to fields, visible help, and progress by section instead of a percentage that lies.

Check your answer

Tick what your own design covered. Pick the level you're interviewing for: each level includes the ones before it.

Junior 0 of 4
Mid-level 0 of 4

References

Concepts behind it

Try next