DEV Community

Doby Baxter
Doby Baxter

Posted on

If GOV.UK Knows the Application Is Invalid, Why Did It Take My Payment?

Maybe this is another one of those posts where I end up roasting the government. But stick with me, because this isn't really a politics post. It's a post about state machines, validation layers, and what happens when you put an irreversible action at the end of a workflow that never once checked whether the user should have been allowed to reach it.

The government part is just the catalyst. The engineering part is the point.

What actually happened

I applied for a British passport on GOV.UK.

Early in the form, it asks something like:

Do you have a certificate of naturalisation or a certificate of registration?

There are a lot of yes/no questions on that journey, and this one behaves exactly like the rest of them. I answered No, and the form did what it always does: it let me continue.

So I continued. I answered the remaining questions. I entered my personal details. I entered my bank details. The system accepted my payment of £107.

Only afterward did I learn that the certificate wasn't supporting evidence I could send in later. It was a prerequisite — something I needed before the application was valid at all.

I asked whether the application could be paused while I resolved the prerequisite. No. I was told I had roughly three months, and that the payment is non-refundable. If I can't complete everything in that window, the money is simply gone.

Here's the part that matters as an engineer: the system had my answer from the very beginning. I told it "No" on question three. It took my money on question twenty-something. Nothing in between used the information it already had.

The problem isn't the question

Asking "Do you have certificate X? Yes/No" is completely fine. That's not the bug.

The bug is what the system does after the answer.

A binary question that gates eligibility should not behave identically to a binary question that merely records a preference. If "No" makes the entire application invalid, then "No" is not informational — it's a hard gate. But the interface gave me no signal that I'd just walked through a gate. It looked like every other question, so I reasonably read it as every other question.

That's a semantic failure dressed up as a UI detail.

Input validation was never the issue

This is where my actual specialization kicks in — I spend most of my working life on configuration, error messaging, validation, and deterministic workflows — so let me be precise about which validation failed, because "the form should validate better" is too vague to be useful.

There are several distinct layers, and they are not interchangeable:

  • Syntactic validation — is this field shaped correctly? (Is the date a date?)
  • Semantic validation — does this value make sense in context? (Is the date in the past?)
  • Business-rule validation — is this allowed by the rules of the domain? (Are you old enough to apply?)
  • Workflow validation — given everything you've told me so far, are you eligible to be at this step at all?
  • Submission validation — is the application in a state where it can legally, financially, and administratively be finalized?

My passport form almost certainly nailed the first three. Every field was well-formed. Nothing was syntactically wrong.

It failed at workflow validation and submission validation — the two layers that actually protect the user from spending money on something that cannot succeed.

Don't let an invalid state propagate

Here's the conceptual shape of what should have happened:

START
  → eligibility questions
  → prerequisite missing
  → [ STOP: application cannot proceed ]
Enter fullscreen mode Exit fullscreen mode

Here's what actually happened:

START
  → answer indicates prerequisite missing
  → continue
  → continue
  → continue
  → payment accepted
  → application submitted
  → "you cannot actually complete this process"
Enter fullscreen mode Exit fullscreen mode

An invalid state that is detectable at step 3 should never be allowed to survive until step 20. In any other production system, we'd call that error propagation — a known-bad condition carried silently downstream until it detonates at the most expensive possible point.

The fix doesn't require anything clever. It doesn't need machine learning or a smarter form. It needs one correctly defined state transition:

if prerequisite_required and not prerequisite_present:
    block_submission()
    explain_why()
    explain_how_to_resolve()
    preserve_progress()
Enter fullscreen mode Exit fullscreen mode

Never gate an irreversible action on an optimistic assumption

Payment is the textbook example of an irreversible action. Money leaves your account. The system tells you it won't come back.

So the condition for accepting payment cannot be:

payment_allowed = user_reached_payment_page
Enter fullscreen mode Exit fullscreen mode

It has to be:

payment_allowed = application_state == READY_FOR_SUBMISSION
Enter fullscreen mode Exit fullscreen mode

Reaching the payment screen is a fact about navigation. Being ready to submit is a fact about eligibility. The system conflated the two, and the user paid for the difference.

Any time an action is irreversible — payment, submission, deletion, dispatch — the system should treat it as a boundary that requires the whole prior state to be provably valid, not merely reachable.

The real world is asynchronous, and the workflow needs to model that

This is the part administrative software chronically gets wrong.

Completing a prerequisite in real life is rarely something the user can just do. It might depend on:

  • an appointment with a third party,
  • a document held by someone else,
  • a certificate that must be requested, issued, posted, scanned, and certified,
  • a process with its own multi-week turnaround.

None of that happens synchronously just because the applicant wants it to.

So a good workflow has to distinguish between two states that look identical on the surface but are completely different:

  • "The user hasn't done this yet." (Their responsibility. Nudge them.)
  • "The user cannot currently do this because another process hasn't completed." (Not their responsibility. Wait, don't penalize.)

A three-month non-refundable countdown that assumes every dependency is under the user's control is modeling the wrong reality. It's charging people for the latency of institutions they don't control.

Accessibility isn't only screen readers

Here's a distinction I wish more teams internalized: accessibility is not just a property of the UI. It's a property of the workflow.

A service can have perfect semantic HTML, keyboard navigation, ARIA labels, screen-reader support, and AAA contrast — and still be functionally inaccessible, because the underlying process is confusing, unforgiving, and impossible to recover from.

A workflow that quietly assumes users can instantly obtain every prerequisite, infer every unstated dependency, remember every required document, and correctly reinterpret an ambiguous yes/no question is already fragile for everyone. For anyone dealing with cognitive load, memory difficulty, limited resources, or dependence on third parties, that fragility stops being an inconvenience and becomes a wall.

You can pass every automated accessibility audit and still ship something that's impossible for a real person to complete safely. GOV.UK's design system is genuinely excellent at the component level. This failure lives one layer up, in the workflow, where the audits don't look.

What a fail-safe version would do

None of this is exotic. Concretely:

  • Dependency-aware questions — a gating answer visibly changes the journey instead of blending in.
  • Hard eligibility gates — invalid state stops at detection, not at checkout.
  • Explicit prerequisite explanations — say why you're blocked and how to fix it.
  • Persistent, resumable progress — don't force people to gamble against a clock they don't control.
  • Pre-payment validation — establish eligibility before taking money, always.
  • Asynchronous dependency states — model "waiting on someone else" as a first-class status.
  • Human escalation paths — a real route to intervene when the machine gets it wrong.
  • Rollback where the system permitted an invalid transaction — if the service let money through in a state it should have blocked, the refund is the system's responsibility, not the user's problem.

The uncomfortable question

So here's where I'll leave it.

If a system lets a user make an error, already holds enough information to recognize the consequences of that error, allows the error to propagate through the entire workflow, accepts payment, and then refuses to let the user recover — where exactly did the error occur?

It did not occur the moment the user clicked the wrong button.

It occurred when the system was designed without a safe state in which that mistake could be recovered from.

That's the bug. Everything else is just where it happened to surface.

Top comments (0)