A Tombstone With a Memory: Keeping Deleted Categories Deleted
A common failure looks like this. Someone deletes a category in the admin screen. An hour later it is back. A sync job had read the old data before the deletion and finished writing after it. The job did nothing wrong by its own information; it simply carried an old picture.
This article proposes a deletion generation witness: a small record that remembers how new a deletion is, so older pictures can be refused. It is a design suggestion, and whether it fits depends on your data store and sync tooling.
The idea
Give each category a generation number that increases on every change, including deletion. When a sync job reads data, it records the generation it saw. When it writes, it presents that number.
When a category is deleted, do not erase everything. Keep a witness holding three things: the category identifier, the generation of the deletion, and the time of deletion. A later write is judged against the witness.
Decision branches
| Incoming write | Compared with witness | Action |
|---|---|---|
| Generation lower than the witness | Older picture | Reject and log |
| Higher generation with an explicit recreate marker from a trusted path | Deliberate return | Apply, and replace the witness with a live record |
| Higher generation, no marker | Unclear intent | Hold for review |
| No generation supplied | Cannot be compared | Hold for review |
| No witness exists | Normal path | Apply as usual |
The recreate marker is what separates a real comeback from a stale batch. Without it, a source that still lists the category would keep reviving it, so decide in advance which system may issue the marker.
Worked hypothetical
A small shop admin has a category called Seasonal. A sync batch reads four category records at 09:00:
- Garden at generation 12, live.
- Seasonal at generation 7, live at the time of reading.
- Gifts at generation 10, with a recreate marker from the admin screen after an earlier deletion at generation 9.
- Outlet with no generation recorded.
Between 09:00 and the write at 09:20, an operator deleted Seasonal, producing a witness at generation 9.
Result when the batch is written:
- Garden: no witness, applied.
- Seasonal: generation 7 is below 9, rejected.
- Gifts: generation 10 is above 9 and carries the marker, applied.
- Outlet: no generation, held.
Totals: 2 applied, 1 rejected, 1 held, 4 records. A batch summary that reports only success would hide the rejection, so report each count.
Retention of the witness
A witness that expires too early reopens the hole. Keep it at least as long as any source could still deliver a picture older than the deletion. If you cannot bound that, keep the witness without expiry and keep it small. Removing witnesses is a separate, reviewed action.
Stop conditions
Pause the sync and ask a person when:
- More than a few rejections arrive for the same category, which suggests a source keeps producing old data.
- Held records accumulate across several runs.
- Generations move backwards for a live record, which would mean the counter itself is unreliable.
Proposed acceptance tests
These are proposals to try in an isolated copy, and none has been run.
- Replay the four records above after creating the witness. Expect the 2, 1, 1 split.
- Delete a category, then send an older update twice. Expect both rejected and the category still absent from listings.
- Send the recreate marker from an untrusted path. Expect a hold, not an apply.
- Check that a deleted category's products are not silently attached to a new category by a stale write.
- Confirm the rejection log names the category, the incoming generation, and the witness generation.
Limits
The witness only helps if every writer presents a generation and the comparison happens at the point of writing. A check made earlier and applied later can still race, so the comparison and the write should belong to one step in your storage layer. Layer it with review of held records rather than treating it as absolute protection.