Define what completion means before accepting the request
A delete button can remove a row while leaving its PDF in object storage, its text in a search index, and its answer in a cache. A later restore can bring the row back. The interface says “deleted”; the system has only hidden one reference.
Model deletion as a durable operation with an explicit scope. Record the target, identity verification, authorization, accepted time, applicable retention policy, and every system expected to respond. This is an engineering design, not a universal statement of legal retention obligations. The responsible business owners must define those obligations for the product.
Separate immediate access revocation, removal from active systems, and expiry from retained backups. They may complete at different times. Product copy and support tooling should describe those states accurately rather than promising instant erasure everywhere.
Build the copy inventory from write paths
Start where data is created and follow every derived artifact. For a document, that may include the original object, extracted text, OCR pages, embeddings, search snippets, preview images, exports, and support attachments. Assign each copy a responsible service and a removal mechanism.
| Copy | Removal evidence | Common blind spot |
|---|---|---|
| Primary database record | Transaction result and target identity | Soft-delete rows remain readable to jobs |
| Object storage | Version-aware deletion result | Historical versions still exist |
| Search index | Generation or document removal receipt | Reindex job republishes old content |
| Cache | Namespace invalidation or bounded expiry | Cached authorization outlives revocation |
| Export | Expiry or deletion confirmation | Long-lived download link remains valid |
| Backup | Retention boundary and restore procedure | Restore reintroduces removed records |
In a versioned S3 bucket, an ordinary delete can create a delete marker while retaining older versions. Removal logic must account for version IDs and configured retention protections. A 404 on the current object does not prove historical bytes were erased. S3 object-version deletion
Fence stale workers before cleaning up
Mark the target as unavailable and advance its lifecycle generation before dispatching cleanup. Workers must compare the generation they started with against the current generation before publishing results. If they differ, publication fails and generated artifacts are discarded.
This prevents a parser that started yesterday from recreating an index entry after today's deletion. It also prevents a retry queue from acting as an accidental backup. The same check belongs at the final write boundary, not only when the job begins.
Do not reuse deleted identifiers for a new customer's data. A delayed deletion event aimed at the old identity could otherwise remove the new object. If identifier reuse is unavoidable, include an immutable incarnation ID in every command and artifact reference.
Use idempotent steps with visible exceptions
Represent cleanup as named steps with state, attempt count, next retry, evidence, and last error. “Already absent” is normally a successful outcome when the target identity has been verified. A permission error is not absence and must remain unresolved.
Avoid one transaction spanning remote services. Commit the deletion request and local access revocation, then dispatch durable tasks. Each task records completion independently. A coordinator determines whether all required active-system steps are satisfied.
Preserve only the minimum audit evidence needed to explain the operation. A deletion log containing the original document body defeats the purpose. Even hashed identifiers can remain sensitive when the input space is predictable; choose evidence formats with the retention and privacy model in mind.
Make restoration deletion-aware
PostgreSQL point-in-time recovery reconstructs a database from a base backup and archived WAL. Restoring to an earlier time can therefore precede a later deletion. That is expected recovery behavior, so deletion records need an independently recoverable path and must be reapplied before exposure. PostgreSQL recovery documentation
Keep the restore environment isolated from outbound email, payment APIs, and indexing consumers. Load the applicable deletion and revocation ledger, reconcile it against restored records, and rebuild derived stores only from the surviving authorized data.
The deletion ledger itself needs a retention decision. Keeping every identity forever is not a free solution. Retain enough information for the lifetime of relevant backups and replicas, then expire it under the agreed policy.
Test resurrection, not only disappearance
Delete a document while extraction runs. Replay an old index event after cleanup. Restore a backup from before the request. Fail one cleanup service for a day, then recover it. Confirm that the user cannot access data during that delay and that the operation remains visibly incomplete.
Report oldest unresolved deletion age, active-copy completion, retained-backup boundaries, and exception ownership. Never convert “timed out” into “complete” to keep a dashboard green. The system's strongest guarantee is an inspectable account of what was removed, what remains under retention, and what prevents it from becoming visible again.
