Players Engine

Devlog · Entry 25 · Engineering Notes

GDPR deletion is a distributed systems problem

· Nic Vannetti · 5 min read

“Users can delete their account” reads like a checkbox. One button, one confirmation dialog, one DELETE statement. I budgeted it accordingly, and then spent a multiple of that budget building what it actually is: a small distributed workflow with scheduling, compensation, and its own failure sweep.

If you're building anything with users in Europe, deletion under GDPR isn't optional, and doing it honestly is harder than it looks. Here's what it turned into for me, and why every shortcut I considered was wrong.

There is no row to delete

The first fantasy to die is “just delete the row.” On this platform, one user's data lives in at least four places:

No single transaction spans those systems. Deletion is therefore not an operation — it's a process, with all the process problems: partial failure, retries, ordering, and observability.

The grace period changed the architecture

Before touching the how, one product-shaped decision reshaped everything technical: deletion has a 30-day grace period. Ask to delete, and the account is immediately deactivated but not destroyed; change your mind within 30 days and it comes back intact.

I didn't add this to be nice (though support-wise, it is — rage-quits and compromised accounts are real). I added it because it converts an irreversible distributed operation into a reversible one, and reversible operations are an order of magnitude safer to operate. During grace, nothing is destroyed, so a bug in the scheduling path destroys nothing. The truly destructive work happens later, in one well-audited purge path, executed by a scheduled job with no human in the loop to fat-finger it.

That means deletion is a state machine, not an action. An account moves through active → pending deletion → purged, or back from pending to active on cancel. Every part of the system had to learn what “pending deletion” means: the account can't be interacted with, doesn't show up where it used to, but its data still exists. Which immediately raises the restore question — deactivation zeroes out those denormalized copies, so cancelling doesn't just flip a flag, it has to rehydrate everything that was zeroed. Restore is a first-class path with its own bugs, not an afterthought. I know because I shipped one of those bugs and got to find it in beta.

Tombstones, or: the graph remembers

The graph database poses a special problem. Other users' worlds reference the deleted node — you can't just vaporize it without deciding what their side of the relationship now means.

The answer is a tombstone: the node's personal data is stripped and the node is reduced to an anonymous marker that keeps referential integrity intact. Other users see a generic deleted-user placeholder instead of broken references or — much worse — someone else's data getting misattributed. The GDPR obligation is to erase personal data, and a tombstone with nothing personal in it satisfies that while keeping the graph structurally sane.

Tombstones also close a subtle security hole. If deleted identifiers are ever recycled or their old routes keep resolving, you can end up serving one user's residue to another. Making “deleted” an explicit, permanent, empty state — rather than an absence — is what lets every code path handle it deliberately instead of accidentally.

The purge, and the sweep behind it

After 30 days, a scheduled job performs the real purge: documents deleted, graph node tombstoned, storage objects removed, the auth record destroyed. Each target system is handled by its own idempotent step, because the job will be interrupted someday and must be safe to re-run. Idempotent here means every step is phrased as “ensure X no longer exists” — a form that can't fail on the second attempt just because the first attempt half-succeeded.

And because every multi-step process eventually strands something in the middle, there's one more component that I'd argue is the least optional of all: a sweep for stuck states. If an account sits in pending deletion past its window plus a healthy margin, that's an invariant violation, and something needs to notice — the difference between finding out from your own monitoring and finding out from a regulator's letter. A user who asked for deletion in March and is still lurking in your database in July, because a purge job died and nothing retried it, is exactly the failure mode this entire design exists to prevent.

The takeaway

If deletion is on your roadmap as a small task, re-estimate it. The honest version is: a state machine with a grace period, a rehydrating restore path, tombstones wherever other users' data references the deleted one, an idempotent multi-system purge, and a sweep that hunts for stuck accounts. None of the pieces are individually hard. What's hard is noticing that all of them are required — and the best time to notice is before your data model has metastasized across four storage systems. Deletion is the feature that audits your entire architecture, whether you scheduled the audit or not.

Want in on a future wave?

Drop your email and we'll let you know when Players Engine opens up. Early supporters get first access.

← All devlog entries