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.
| Question | What we assume | What it changes |
|---|---|---|
| Which products and markets? | Car and home, 12 US states plus the UK, more later | The questions must be data per product, market and date, not code |
| Who changes the questions? | Underwriting and compliance teams, often monthly | We 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 answers | A 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 insurance | Consent 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 answers | An audit trail and an immutable snapshot at submit |
| How sensitive is the data? | Driving offences, claims, health conditions, national ids | Field-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.
- 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.
- 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.
- 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.
- 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.
- 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.
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?
Because many questions repeat. "Date of birth" exists once per driver, so the list depends on a count the customer enters. A flat list can't express "one of these per driver".
Also, which questions are shown, which are rating factors, and which end the journey are underwriting rules that change by state and date. Hard-coding them as flags in the client means a release for every rule change, and old clients would show new rules to people who started under the old ones.
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?
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.
{
"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" } }
]
}]
}
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.
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.
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?
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.
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.
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);
}
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?
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.
Consent before the check
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?
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.
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"
};
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.
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?
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.
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
| Action | Customer | Agent | Why |
|---|---|---|---|
| Answer questions | Yes | Yes, stamped as agent | Both are normal; the record says who |
| See full national id or health details | Yes, after re-auth | Masked unless their role allows | Least privilege on PII |
| Acknowledge customer disclosures | Yes | No, only record that they were read aloud | The customer must receive them |
| Sign the declaration | Yes | No | Attestation is personal |
| Override a knockout | No | Only underwriters, with a reason | A 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?
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.
Resume links
"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_answeredwith 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, neverlocalStorage, and clear it on sign-out, submit and timeout. - Correct
autocompletetokens 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.
| Part | Responsibility | Why it's separate |
|---|---|---|
| Schema loader | Fetches the pinned schema version by id, caches it as immutable | Schema changes rarely and is large; cache it apart from answers |
| Rule engine | Evaluates JSONLogic rules: visibility, repeats, knockouts, validation | Pure and shared with the server, so it's testable alone |
| Answer store | Answers by path, revision counter, dirty set of uncommitted fields | One source for the renderer, the save queue and the quote controller |
| Section renderer | Renders the current section from the visible graph with the question type registry | Generic; the same code serves car, home and life |
| Save queue | Batches committed answer events, retries with the same clientEventId, handles 409 | Saving must not block typing, and retries must not double-record |
| Quote controller | Debounces rating answers, cancels stale calls, holds the price state machine | Pricing has its own timing and failure modes |
| Session guard | Idle timer, warning dialog, tab coordination, PII clearing | Security behaviour in one auditable place |
| Lookup client | Prefill calls, consent checks, mismatch results | Each 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.
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.
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[] };
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
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 }
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?
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, andaria-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
fieldsetwhoselegendis 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?
Usually nothing: their quote is pinned to the schema version it started on, so they keep seeing the wording that was in effect then, which is the wording that was filed for that date.
If the change must apply immediately (a regulator-ordered correction), the server returns SCHEMA_SUPERSEDED. The client migrates unchanged questions, asks the changed question again because the old answer was given to different words, and records both versions in the log.
Think first
The customer reviewed a price of £91, then the insurer published new rates before they signed. What should the submit do?
Submit carries the revision the customer saw. The server re-rates, and if the price differs it refuses with PRICE_CHANGED and the new price. The client shows the new price clearly, explains why with the reason codes, and asks for confirmation. It never binds at a price the customer didn't see.
Think first
Should the application work offline?
Partly. Answers already in memory can be edited offline, and the save queue holds events with their clientEventId until the connection returns. But pricing, lookups and submit need the server, and holding health data in IndexedDB on a possibly shared device is a risk most insurers won't accept. So: tolerate short drops, show a clear offline banner, and never claim a price or a submit happened while 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.
References
- Understanding WCAG 2.2: 2.2.1 Timing Adjustable Warn before a time limit, at least 20 seconds to extend, at least ten times.
- Understanding WCAG 2.2: 2.2.5 Re-authenticating Continue without losing data after signing in again.
- Understanding WCAG 2.2: 2.2.6 Timeouts Warn how long inactivity can last before data is lost, unless data is kept for over 20 hours.
- Understanding WCAG 2.2: 3.3.4 Error Prevention (Legal, Financial, Data) Reversible, checked, or confirmed: why the check answers page matters.
- Understanding WCAG 2.2: 3.3.7 Redundant Entry Don't make people type again what they already gave in the same process.
- GOV.UK Design System: Error summary The pattern for errors at the top of the page, linked to fields.
- GOV.UK Design System: Check answers The review page with Change links before a legal submit.
- GOV.UK Design System: Question pages One thing per page and when to group questions.
- JsonLogic The rule format shared by browser and server, and how to add custom operations.
- MDN: HTML autocomplete attribute The list of autofill tokens for names, dates of birth, addresses and contact details.
- FCA Handbook: ICOBS 7.1 The right to cancel The 14-day and 30-day cancellation periods for UK consumer insurance, and their exceptions.
Concepts behind it
- Config-driven UIDescribing screens as JSON that names known components, rendered through an allowlisted registry, so layouts change without an app release: safe only if configs are validated, never executed, and older clients get fallbacks.
- Forms at scaleHow to hold, validate, and submit form data so users see the right error at the right moment, never lose what they typed, and get the same rules on the client and the server.
- Request races and cancellationResponses can come back in any order, so a page can show the answer to an old question; learn to let only the latest answer reach the screen and to cancel the work nobody needs.
- Validating and sanitizing inputChecks in the browser make forms friendly; checks on the server keep the data and the system safe, from file uploads to redirect URLs to the URLs your server fetches for users.
- State machines for UIModel a flow as a fixed set of states and the events that move between them, so impossible combinations like "loading and failed at once" can't happen and every button does the right thing in every state.
- Client storage optionsThe six places a web app can keep data on the user's device (cookies, localStorage, sessionStorage, IndexedDB, the Cache API, and OPFS), how big, how fast, and how long-lived each one is, and how to pick the right one for each kind of data.
- Focus managementDeciding where keyboard focus goes when the page changes under the user (route changes, modals, deletes, new content) so keyboard and screen reader users never lose their place.
- API contracts, versioning, and errorsThe written agreement between the frontend and the API: a typed schema both sides are checked against, rules for changing it without breaking old clients, and one error shape the UI can always understand, including for partial failures and safe retries.