A customer asks you to delete their data. You run the deletion, the record disappears from the interface, and you reply confirming it is done.
It is not done. The row is soft deleted and still present, a replica has it, the search index has it, last night's backup has it, the analytics warehouse has a copy with a different primary key, three log entries contain it, and two vendors you forwarded it to still hold it.
This post is what deletion actually requires and what you are honestly able to promise. For whoever has to implement the request and whoever has to answer for it.
Soft delete is not deletion
A deleted_at timestamp hides a record from queries. The data is present, readable by anyone with database access, and included in every backup taken since.
Soft delete is a good pattern for undo and for referential integrity. It is not deletion, and the failure is describing it as though it were. If your product has a deletion feature and it sets a flag, be clear internally about that, because eventually someone will tell a customer or a regulator that the data is gone.
Enumerate the copies
The record exists in more places than the application knows about. The list is longer than most teams expect and it is specific to your architecture, which is why it has to be written down once rather than reconstructed each time:
- Read replicas, which will converge, and restored environments, which will not.
- Search indices. Deleting from the database does not remove the document.
- Caches, application and CDN, holding rendered responses.
- The analytics warehouse, usually loaded by a pipeline with its own copy and its own keys.
- Object storage: uploads, generated exports, avatars.
- Message queues and event streams, where an event carrying the data may be retained for days.
- Logs, application and audit, containing identifiers and sometimes content.
- Backups and snapshots, the hardest case.
- Third parties: your email sender, support tool, error tracker, analytics vendor, model provider.
- Support tickets and internal documents, where somebody pasted a record while debugging.
The last two are where completeness usually fails, because neither is in the application's data model.
Backups are the honest exception
You cannot surgically remove one person's data from an immutable backup without destroying the backup's integrity, and nobody expects you to.
The accepted approach is to bound it by retention: the data persists in backups until those backups age out, on a stated schedule, and you undertake not to restore the deleted data into production if a restore happens. That undertaking needs an actual mechanism, usually a suppression list that is re-applied after any restore, or the promise is empty.
State this position in your privacy documentation rather than leaving it implicit. Regulators and enterprise reviewers are familiar with it. What they object to is a claim of immediate complete erasure that is not true.
Keep what you are required to keep
Deletion is not unconditional, and over-deleting creates its own problems.
Invoices and transaction records usually carry a statutory retention period. Audit logs may be required for a compliance obligation, and they are also the record of the deletion itself. Anything under legal hold must be preserved.
The reconciliation is anonymisation rather than removal: keep the invoice with the transaction detail, remove the personal identifiers, and document which fields you retained and why. Get this written down before the first request rather than deciding it under a deadline.
Anonymisation has to be irreversible
Replacing a name with user_4812 is pseudonymisation, not anonymisation, if a mapping table still exists. It remains personal data and remains in scope.
Real anonymisation means the mapping is destroyed and re-identification is not possible from the remaining fields, which is harder than it sounds when the remaining fields include a rare combination of attributes. If you are relying on anonymisation to satisfy a deletion obligation, be sure it meets that bar rather than the appearance of it.
Build it as a pipeline, not a script
A one-off deletion script run by an engineer is unrepeatable, unverifiable, and will drift from the architecture within a quarter.
What holds up: a deletion request record, a defined set of handlers (one per system), each idempotent, each reporting completion, and an overall status with evidence. New system, new handler, and the absence of a handler is visible.
This also gives you the thing you will be asked for: a record showing when the request arrived, what ran, what completed, and what remains pending until backups expire.
Test it properly
Create a user, exercise the product thoroughly, delete them, then go looking:
-- the obvious place
SELECT count(*) FROM users WHERE id = :id; -- expect 0, not deleted_at set
-- the less obvious ones
SELECT count(*) FROM audit_log WHERE actor_id = :id;
SELECT count(*) FROM events WHERE payload::text LIKE '%'||:email||'%';
Then search your search index, your warehouse, your object storage and your logs for the address. Doing this once, thoroughly, is how you find out which copies your list is missing. Most teams find at least two.
The concession
Complete deletion across a mature architecture is genuinely hard, and a team promising immediate erasure everywhere is either small, new, or mistaken. The regulations account for this: the obligation is to act without undue delay and within a month, not instantly, and the backup position above is well understood.
So the objective is not perfection. It is an accurate statement of what you do, executed reliably, with the exceptions named. A vendor who says "removed from production systems within seven days, purged from backups within thirty-five, invoices retained for seven years in anonymised form" is more credible than one claiming everything is gone immediately, and considerably easier to defend.
The implication
The question a reviewer asks is not whether you can delete a user. It is whether you can name every place their data lives.
If you cannot produce that list, your deletion is best-effort regardless of how good the code is, and the list is the artifact worth building first.