DEV Community

Ivan Rossouw
Ivan Rossouw

Posted on AI-assisted

Dismiss the Failure You Saw, Not the One That Arrived Later

An operations queue often looks like ordinary CRUD: load a list, show a few details, and let an authorised person dismiss a resolved item. That appearance is misleading. Once a record can change between rendering and confirmation, the dismiss button becomes a concurrency boundary.

The subtle bug is not merely that two writes overlap. It is that the system can perform a valid write against the wrong occurrence of a failure. The key remains the same, so nothing looks obviously inconsistent. Yet newer evidence disappears and the audit trail tells a story about an older state.

One key can describe several occurrences

Imagine a worker records failure A for a particular operation. An operator opens the queue and reads it. Before the click reaches the server, the worker tries again and records failure B under the same logical key.

If dismissal means “delete whatever has this key,” it removes B. The operator never saw B and never decided it was safe to retire. The command succeeded technically while violating the human intent behind it.

Confirmation dialogs make this window wider. Bulk actions make the consequence larger. “Dismiss all” can quietly change from “dismiss the failures on my screen” to “dismiss whatever happens to exist when I confirm.” Those are different contracts.

The command needs the observation

The useful design shift is to send both identity and observation. The key answers, “Which logical item?” The observed revision answers, “Which occurrence did the person review?”

A revision can be a database row version, an opaque concurrency token, or an application-defined value built from fields that change with every meaningful update. The representation matters less than the invariant: two materially different occurrences must not share a revision.

The reviewed implementation derived a lightweight revision from an attempt counter and the most recent failure time. That choice fit its persisted model, but it also creates an obligation: every writer that refreshes a failure must advance those inputs. If one path forgets, the version boundary weakens.

Compare inside the persistence boundary

The server should not trust the screen’s snapshot as current truth. It should reload current state, compare it with the observation, and conditionally stage the mutation.

for each observed item:
    current = load by stable key

    if current is missing:
        mark already_gone
    else if current.revision differs from observed.revision:
        keep current
        mark superseded
    else:
        stage removal of current
        mark cleared

save staged removals
return cleared, superseded, already_gone
Enter fullscreen mode Exit fullscreen mode

This is version-aware conditional mutation. It has compare-and-swap semantics at the application level, but that phrase should not hide the database details. A real relational interleaving may also need a native concurrency token, a suitable isolation level, or explicit conflict handling around the final save.

Bulk means the rendered snapshot

It is tempting for a bulk command to reread the queue after confirmation and then remove everything it finds. That is convenient, but it discards the observation that justified the action.

Pass the rendered collection instead. Compare each observed revision with its current counterpart. Clear matching occurrences together where the storage model allows it, and return changed occurrences separately.

Partial success is not an awkward edge case here; it is the truthful result. If nine items are unchanged and one has failed again, retiring nine and preserving one matches the operator’s intent better than either clearing all ten or rejecting the whole selection without explanation.

Make superseded a first-class result

A Boolean result cannot describe this outcome well. “Success” hides retained items; “failure” suggests that nothing useful happened.

Use a result that separates cleared, superseded, and already-absent items. The interface can then say, in plain language, that newer failures were kept for review. It should also refresh those items so the operator sees the current evidence, not the stale snapshot.

This richer result is more work. It touches command contracts, persistence code, UI messages, localisation, tests, and sometimes telemetry. The benefit is that the system stops pretending a mixed outcome is all-or-nothing.

Audit what actually changed

Audit records should be based on the occurrences the persistence layer reports as cleared, not merely on what the caller requested. Otherwise, a rejected stale action can still leave an audit entry claiming that something was dismissed.

There is a second boundary to consider: if the business requires the audit and mutation to succeed atomically, write them in the same transaction or publish through a transactional outbox. A best-effort audit performed after the clear is useful operational evidence, but it is not the same guarantee. Naming that limit is part of honest engineering.

Test distinctions, not just happy paths

The most valuable tests separate states that a key-only implementation would collapse:

  • an unchanged occurrence is cleared;
  • the same key with a newer revision is retained;
  • a mixed bulk snapshot clears only unchanged occurrences;
  • a later failure time is considered new even if the attempt counter is unchanged;
  • a stale action produces no dismissal audit for the retained occurrence.

Mutation testing is especially useful. Remove the revision comparison, ignore one revision component, or clear superseded items. If the tests still pass, they prove the shape of the code rather than the concurrency contract.

Sequential in-memory tests still have limits. They can prove decision logic, but they do not reproduce a relational database race. Add provider-level coverage when the risk justifies it, and keep claims bounded until that evidence exists.

The practical review question

Search your administrative tools for verbs such as dismiss, approve, resolve, retry, archive, and overwrite. For each one, ask: does this command act on a stable identity, or on the exact state the person reviewed?

Using only a key is sometimes correct. When new evidence can arrive under that key, however, version-aware actions add a small amount of friction in exchange for preserving facts and keeping human intent intact.

Top comments (0)