QuestionsSystem Design

Design an optimistic like/save button

Optimistic UIEasySystem Design

Design a like (or save/follow) button that feels instant. Apply RADIO; cover rollback and rapid clicks.

What it tests

Optimistic updates, exact rollback, request dedup, and idempotency for high-frequency micro-interactions.

Approach & answer

Requirements: a toggle that feels instant, ends in the correct state, survives failure and rapid clicks, and is accessible. Architecture: on click, immediately update local UI state (optimistic), fire the request in the background, and reconcile when it resolves — on error, roll back to the previous value and surface a subtle notice. Data model: keep the displayed value plus the last server-confirmed value so rollback is exact; track an in-flight flag per item. Interface: POST /like { id, liked } returning the authoritative state/count. Optimizations: dedup or cancel concurrent requests for the same item; debounce rapid toggles or send only the final intent; keep the control clickable but reflect pending state; make it an accessible toggle (aria-pressed). Why optimistic: perceived latency dominates UX for micro-interactions (like, star, follow) — waiting a round-trip makes them feel broken. The correctness trap is reconciliation: store the pre-update value so a failed request restores exactly it, not a guessed value, and handle the race where the user toggles twice before the first response returns (sequence responses, or cancel superseded requests). Prefer sending the desired END-STATE rather than a delta so retries are idempotent. Roll back visibly but gently — revert the icon and show a toast — so the user knows it didn't persist.

Use this technique when

Like/star/follow/save toggles, and any high-frequency action where a round-trip wait would feel broken.

Code

function LikeButton({ id, liked, count, save }) {
  const [state, setState] = React.useState({ liked, count });

  async function toggle() {
    const prev = state;                                  // remember for rollback
    const next = { liked: !prev.liked,
                   count: prev.count + (prev.liked ? -1 : 1) };
    setState(next);                                      // optimistic
    try {
      const server = await save(id, next.liked);         // send end-state (idempotent)
      setState({ liked: server.liked, count: server.count });
    } catch {
      setState(prev);                                    // rollback to exact prior value
    }
  }

  return (
    <button aria-pressed={state.liked} onClick={toggle}>
      ♥ {state.count}
    </button>
  );
}

References