what users feel - backend craft beyond the dashboard

a green dashboard doesn't mean a happy user. four backend invariants - idempotency, state-preserving errors, cancellation and lean delivery - named after the feeling each one gives the person on the other side of the screen.

notes from a talk on backend craft, and why a green dashboard doesn’t mean a happy user.

the dashboard says we’re fine

99.99% uptime. sub-50ms p99. clean query plans. zero unhandled exceptions.

and yet the person on the other side of the screen can still be staring at a frozen button on a train, wondering if they just paid twice.

observability measures what the system did, not what the user felt. most apis call it done the moment bytes leave the socket - but that’s exactly where the real world starts: dropped packets in tunnels, budget phones choking on huge json, anxious double-taps on a lagging screen.

so here’s the reframe: done isn’t when the query returns. done is when the user’s state has safely settled.

four feelings, four invariants

each one is named after how it makes the user feel.

certainty - idempotency

a retry should never mean a double charge. put an Idempotency-Key on every mutating endpoint, deduplicate atomically before any side effect (redis SET NX EX, or a sql unique constraint), and replay the stored response when the same key comes back.

a disabled submit button is ui sugar, not a security boundary.

relief - state-preserving contracts

when validation fails, the form should keep what the user typed. return problem details with field-level errors, so the client can point at the one field that’s wrong instead of wiping the page. partial saves and drafts help too - collect input first, validate strictly later.

calm - cancellation that goes all the way down

when the user navigates away, the work should stop. propagate the disconnect - context.Context in go, res.on("close") in node, the disconnect event in asgi - all the way to the database, and cancel the query. nobody is waiting for that result anymore.

speed - delivery over raw payload

a 12ms query can still mean a 1.8s frozen screen on a budget phone. send lean view models instead of whole tables, stream when you can (chunked transfer, sse, progressive hydration), and lean on cache semantics like stale-while-revalidate and conditional etags.

so

keep measuring uptime and latency. just treat them as proxies for the thing that actually matters - what the user feels.

a 200 OK tells you the server finished. it doesn’t tell you the user did.

four questions for your next api review

  1. if the client retries this post, what happens?
  2. when validation fails, does the form keep its state?
  3. when the socket closes, does the query stop?
  4. how long does this payload take to parse on a budget phone?

similar write-up for reference: the web is the product, not the pitch