Skip to content

Checking that a count did not change — a sanity check before writing a reorder API result

Building a drag-and-drop reorder feature for a list of items means sending the new order to the server and saving it. If there’s even a small flaw in the reordering logic, that save can quietly persist data with items missing or duplicated.

Note: A “sanity check” here means a lightweight check that the result of a process satisfies a condition that should obviously hold — not a thorough validation, just a last-resort check for “is this clearly wrong?”

The shape of the reorder logic

The new order from a drag-and-drop interaction arrives at the server as an array of IDs. The server reorders the existing data to match.

# Place items according to the order sent by the client
reordered = []
placed_ids = set()
for sid in dedup_order:
    if sid in by_id:
        reordered.append(by_id[sid])
        placed_ids.add(sid)

# Anything not covered by the order gets appended,
# keeping its existing relative order
for site in data:
    sid = str(site.get('_id', '')).strip()
    if sid and sid in placed_ids:
        continue
    reordered.append(site)

This two-step structure makes sense on its own. The order the client sends might only cover a filtered or searched subset of the data. Appending whatever wasn’t covered by that order, while preserving its existing relative position, prevents items that weren’t currently visible from disappearing.

The risk that remains

The problem is what happens if the combination of “place in order” and “append the rest” has a flaw of its own. ID deduplication, string-conversion edge cases, malformed entries — the more complex this logic gets, the more the underlying assumption (“every item appears exactly once in the output”) is at risk of breaking. If there’s a bug here, the reordered data could end up shorter than the original (items lost) or longer (items duplicated).

The fix — verify the count matches right before writing

Right before writing the reordered result to disk, check one more thing: does the input count match the output count?

# Sanity check: counts must match (no items lost or duplicated)
if len(reordered) != len(data):
    app.logger.error(
        'reorder_sites: count mismatch (in=%d, out=%d) — refusing to write',
        len(data), len(reordered),
    )
    return jsonify({
        'status': 'error',
        'message': 'Reorder count mismatch; refusing to save',
    }), 500

_atomic_write_json(filepath, reordered)

If the counts don’t match, the error log records both the input and output counts, and the write itself is aborted. There’s no need to pinpoint exactly where the reorder logic went wrong — whether the result is safe to save can be determined mechanically.

Why a count match is enough of a defense

This reorder operation doesn’t change the content of any item — it only changes the order. That means before and after, the set of elements should always stay the same. If the count changed, something either disappeared or got duplicated. A matching count doesn’t prove the content is perfectly intact, but as a lightweight way to catch obvious corruption — items lost or duplicated — comparing counts does the job.

What this design gets right

This isn’t about writing the reorder logic perfectly — it’s about catching a flaw in that logic before it turns into data corruption, even when the logic itself isn’t perfect. The more complex a piece of logic gets, the harder it is to cover every case in tests. Identifying one invariant that should always hold before and after, and checking only that at the very end, acts as insurance against bugs that tests didn’t anticipate.

Summary

Aspect Detail
Feature An API that takes a drag-and-drop reorder result as an array of IDs and reorders existing data to match
Risk A flaw in the two-step “place in order, then append the rest” logic can cause data loss or duplication
Fix Compare input and output counts right before writing; abort and return an error on mismatch
Design principle Not aiming for perfect logic, but mechanically preventing a write while an invariant is broken

Code that rewrites a batch of items at once tends to be written assuming the logic works correctly. Thinking through what happens when that assumption breaks is what actually protects the data. The trap of a word like “done” lumping together multiple distinct meanings shows up again in the incident that conflated deploy-complete with git-commit-complete.