To delete a person's data reliably in a distributed system, treat each deletion request as a durable, tenant-scoped operation. Fan it out to every system that holds personal data. Mark each system done only after you have verified the data is gone, not when an API returned HTTP 200. Anything you could not confirm stays unknown and gets an owner and a deadline. That is the core of this article.
Most teams start with a single DELETE statement against the primary database. That covers one store. The same person's data may also sit in read replicas, event logs, search indexes, caches, object storage, a data warehouse, vector indexes, application logs, third-party SaaS tools and backups. A deletion that touches one store and reports success is worse than an honest failure, because everyone believes the job is finished.
This article is for the tech lead or solution architect who has to build the workflow, not just describe it. It is engineering guidance, not legal advice. The legal examples (GDPR and CCPA) show why the workflow needs clocks and evidence. Your actual obligations depend on your role, your jurisdictions and the exemptions that apply to you, so confirm them with counsel.
The design below is a proposed design. It is not a proprietary method that Developers.dev claims to run in every client environment. It combines patterns you may already know: the outbox pattern, idempotency keys and reconcile-before-retry. It applies them to a problem where a silent failure carries regulatory and trust consequences.
Key Takeaways
- Deletion is a workflow, not a statement. Model it as a durable operation with one task per system that holds personal data.
- "Accepted" is not "erased". A connector reports done only after a verification step, such as a subject lookup that returns absent. A 200 response does not count.
- Unknown is a first-class state. A timeout or an indeterminate response means completion could not be confirmed. It is neither success nor verified failure. Reconcile before you retry.
- Bind each operation to an immutable approved scope. Use a tenant-scoped operation ID plus a payload hash. Reject a reused ID with a changed payload. Use a subject epoch so a delayed retry never deletes a legitimately re-created account.
- Write intake atomically. The operation, its per-system tasks and the schedule go into one transaction before you acknowledge the request.
- Terminal failure needs a reason and an owner. Legal holds, vendor limits and unreachable systems each need an explicit state, not a silent retry loop.
- Identifiers are personal data too. Your deletion ledger must not become the next place a person's email lives forever.
- Readiness is a set of go/no-go controls. All of them must pass. A summed score cannot certify a deletion system, and neither can this article.
Why Erasure Becomes a Distributed-Systems Problem
Key takeaway: The hard part of deletion is not the delete. It is knowing every place the data went, and proving it is gone from each one.
In a monolith with one database, deletion is a transaction. In a system built from services, queues, indexes and vendors, personal data is copied by design. Each copy exists for a good reason: read performance, decoupling, analytics, search, resilience. Each copy also has its own deletion semantics, and some have none.
The table below maps common stores to the usual deletion mechanism and to the reason a successful delete call does not prove the data is gone. The mechanisms are general patterns. Check the behavior of your specific product in its own documentation.
| Store | Typical deletion mechanism | Why "delete returned success" is not enough | Verification evidence |
|---|---|---|---|
| Primary relational database | Row delete or anonymization | Soft-delete flags leave data in place. Replicas, CDC streams and audit tables keep copies. | Subject lookup returns zero rows on primary. CDC consumers confirmed. |
| Event log (Kafka-style) | Key-based compaction with tombstones, retention expiry, or crypto-shredding | Compaction runs in the background and only applies to keyed topics. Tombstones are themselves retained for a period. Downstream consumers hold their own copies. | Topic policy documented. Consumer-side deletes confirmed. Retention window recorded. |
| Object storage | Delete object or delete specific versions | On a versioned bucket, a plain delete typically adds a delete marker and keeps prior versions. See the Amazon S3 versioning documentation. | Version listing for the subject prefix is empty. |
| Search index and cache | Delete by document key, TTL expiry | A reindex from a stale source can resurrect the document. Caches may serve old copies until expiry. | Query by subject returns nothing. Reindex source confirmed clean. |
| Warehouse and data lake | Partition rewrite, table-level delete, or masking | Append-only files and snapshots retain old values. Derived tables copy fields. | Lineage-based scan finds no subject reference. |
| Vector index, RAG corpus, agent memory | Delete chunks and embeddings by source document ID | Embeddings and summaries are derived data. They may not carry a subject key unless you designed for it. | Source-document-to-chunk mapping shows zero remaining chunks. |
| Logs and traces | Retention expiry, redaction pipeline | Free-text fields leak identifiers. Logs often outlive the application data. | Retention policy documented. Redaction coverage tested. |
| Third-party SaaS | Vendor API, admin console or contractual request | Many vendor deletions are asynchronous. Accepted does not mean completed. | Vendor completion receipt or a post-request lookup. |
| Backups and snapshots | Expire on retention schedule | A restore can resurrect deleted data. | Documented retention. Restore procedure that replays deletions. |
Identifiers are sensitive data too
Engineers often assume an internal user ID or a hashed email is safe to spread. Under GDPR, Recital 26 says pseudonymised data that could be attributed to a person with additional information should still be treated as information on an identifiable person. A device ID, an email hash or an internal customer ID may qualify, depending on who can link it back. That has a practical consequence. The identifiers your erasure workflow uses to find data can themselves persist in logs, queues and the deletion ledger. Design the workflow so it propagates an opaque internal reference instead of raw email addresses, and treat the reference-to-identity mapping as the most sensitive table in the system.
If your services already communicate through events, the schema and governance practices in event-driven architecture governance are where subject references get standardized. Without a standard subject key, every erasure becomes a custom search.
How Most Teams Approach Deletion, and Why It Fails
Key takeaway: The usual approaches fail for the same reason. They define "done" as "we sent the request", not "we confirmed the outcome".
These are the five approaches we see most often, with the property that breaks each one:
- A ticket and a runbook. It works at ten requests a month. It has no owner for the unlisted store, no evidence trail and no clock, so it degrades silently as the system grows.
- Hard delete in the primary database only. Fast to build, and the data lives on everywhere else.
- Publish a
UserDeletedevent and trust subscribers. Event delivery is not completion. A subscriber that crashes after consuming the event, or one that was never subscribed, produces no signal at all. - Soft delete with a flag. The data is still stored, still backed up, still indexed and often still reachable by a reporting query that forgot the filter.
- "The vendor handles it." Possibly true, but you need the vendor's completion evidence and a path for when their job fails.
The clocks are real, so the workflow needs a clock
Regulations give deletion a time dimension. These are examples, not a statement of your obligations:
| Regime (example) | What the primary text says | Engineering implication |
|---|---|---|
| GDPR Article 17 and Article 12(3) | The controller must erase personal data without undue delay where a listed ground applies. Information on action taken must be provided within one month of receipt, extendable by two further months where necessary, with the data subject informed of the extension and its reasons. See the GDPR text on EUR-Lex. | Every operation needs a received-at timestamp, a due-at timestamp and a recorded reason if the deadline is extended. |
| CCPA, Civil Code 1798.105 | A business that receives a verifiable request must delete the personal information from its records, notify service providers and contractors, and notify third parties to whom it sold or shared the data, unless this proves impossible or involves disproportionate effort. See the statute text. The California Attorney General's CCPA page describes a 45-calendar-day response period with a possible 45-day extension on notice. | The workflow must fan out to downstream processors and record the "impossible or disproportionate effort" reasoning when it applies. That reasoning is a terminal state with an explanation, not a missing row. |
Whether you are a controller or a processor, a business or a service provider, and which exemptions apply, are questions for your counsel. The workflow's job is to produce the facts counsel needs: what was requested, when, what was deleted, what could not be, and why.
Notice what the primary texts do not say. Neither tells you how to prove a Kafka topic is clean or how to confirm a vendor job ran. That gap is the engineering problem, and it is where the design below spends its effort.
A Proposed Design: The Erasure Ledger and Per-System Connectors
Key takeaway: One durable operation, one task per system, and a connector contract that forces every system to answer "verify", not just "delete".
The proposed design has five parts. It is a design to evaluate against your own constraints, not a drop-in product.
- Intake. Authenticates and verifies the requester, resolves them to an internal subject reference, and writes the operation. Ambiguous identity stops here.
- Erasure ledger. A durable store of operations and per-system tasks. It holds state, not personal data.
- Connectors. One per data store, each implementing the same four-method contract:
freeze,delete,verify,evidence. - Reconciler. Re-checks tasks that are requested or unknown and moves them to a verified or terminal state.
- Escalation. Every task that is unknown, exempt or terminally failed has a named owner and a deadline tied to the operation's due date.
request -> [intake: verify requester, resolve subject_ref] -> ONE transaction: operation + tasks (one per system) + schedule -> ack to requester (only after commit) per task: freeze -> delete (conditional) -> verify -> evidence | | timeout/unclear absent? -> VERIFIED_ABSENT v | UNKNOWN <-- reconcile --+-- present? -> retry (bounded) operation closes only when every task is terminal: VERIFIED_ABSENT | EXEMPT (reason, owner) | FAILED_TERMINAL (reason, owner)Task states and what each one means
| State | Meaning | Terminal? | Who acts next |
|---|---|---|---|
| PENDING | Task created, not yet claimed. | No | Worker |
| CLAIMED | A worker holds a lease. | No | Worker or lease expiry |
| REQUESTED | The system accepted the request, such as an asynchronous vendor job. Completion is not confirmed. | No | Reconciler |
| VERIFIED_ABSENT | A verification step confirmed the subject's data is absent, and evidence is stored. | Yes (success) | None |
| UNKNOWN | Completion could not be confirmed. This is not success and not verified failure. | No | Reconciler, then escalation owner at the deadline |
| EXEMPT | Data retained for a stated, approved reason, such as a legal hold or retention obligation. | Yes (with reason) | Privacy owner reviews |
| FAILED_TERMINAL | The system cannot delete (for example, vendor limitation). Requires human decision. | Yes (failure) | System owner and privacy owner |
The key discipline is that no connector may write VERIFIED_ABSENT from a delete response alone. The verify step is a separate read that asks the system of record what is true now. If the system cannot be read, the answer is UNKNOWN.
Privacy ownership belongs to a person, not a service
Each store in your data inventory needs two named owners: the engineering owner who maintains the connector and the privacy owner who decides on exemptions. A connector without an owner is a store that will silently stop working. The same applies to the ledger: restrict who can read it, minimize what it stores (opaque subject reference, system IDs, states, evidence pointers), and set its own retention period with counsel's input. Regulations such as the CCPA allow a limited confidential record of deletion requests for specified purposes. Check what applies to you before deciding how long to keep one.
Tenant-Safe Operation Identity, Atomic Intake and Safe Claiming
Key takeaway: Identity, payload hash, task creation and scheduling must commit together. Otherwise a crash between steps leaves a request nobody is working on, or two workers working it.
Deletion looks naturally idempotent, since deleting something twice should be harmless. The danger is not the second delete. It is the wrong delete: a delayed retry that removes a legitimately re-created account, or a replayed request with a changed scope that silently widens what gets erased. For consequential mutations like this, the design needs four properties:
- A durable, tenant-scoped operation identity. The key is
(tenant_id, operation_id). A bareoperation_idcan collide across tenants. - An immutable approved payload. Hash the approved scope (subject reference, subject epoch, systems in scope, cutoff) and store the hash with the operation. A repeat of the same ID with a different hash is rejected, not merged.
- Atomic claim. Workers claim tasks with a lease so two workers never run the same task concurrently.
- A way to tell legitimate later operations apart. A person who deletes an account and registers again gets a new
subject_epoch. A new deletion request later is a newoperation_id. Old retries are bound to the old epoch and cannot touch the new data.
The SQL below is illustrative and PostgreSQL-flavored. It is not tied to any vendor's API contract.
CREATE TABLE erasure_operation ( tenant_id uuid NOT NULL, operation_id uuid NOT NULL, subject_ref text NOT NULL, -- opaque internal reference, not an email subject_epoch bigint NOT NULL, -- increments when an account is re-created scope_hash bytea NOT NULL, -- hash of the approved scope; never updated status text NOT NULL DEFAULT 'OPEN', received_at timestamptz NOT NULL, due_at timestamptz NOT NULL, PRIMARY KEY (tenant_id, operation_id) ); CREATE TABLE erasure_task ( tenant_id uuid NOT NULL, operation_id uuid NOT NULL, system_id text NOT NULL, state text NOT NULL DEFAULT 'PENDING', attempt int NOT NULL DEFAULT 0, lease_owner text, lease_expires_at timestamptz, evidence_ref text, reason_code text, updated_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (tenant_id, operation_id, system_id), FOREIGN KEY (tenant_id, operation_id) REFERENCES erasure_operation (tenant_id, operation_id) ); -- Intake: one transaction. If it commits, the work is durable and scheduled. BEGIN; INSERT INTO erasure_operation (...) VALUES (...) ON CONFLICT (tenant_id, operation_id) DO NOTHING RETURNING scope_hash; -- If no row returned: SELECT scope_hash for the key and compare. -- same hash -> return the existing operation (safe replay) -- different -> reject with a conflict; never merge or overwrite INSERT INTO erasure_task (tenant_id, operation_id, system_id) SELECT ..., system_id FROM data_inventory WHERE holds_personal_data; COMMIT; -- Acknowledge the requester only after COMMIT succeeds. -- Claim: concurrent workers cannot take the same task. UPDATE erasure_task t SET state = 'CLAIMED', lease_owner = $1, lease_expires_at = now() + interval '5 minutes', attempt = attempt + 1, updated_at = now() WHERE (t.tenant_id, t.operation_id, t.system_id) = ( SELECT tenant_id, operation_id, system_id FROM erasure_task WHERE state IN ('PENDING', 'REQUESTED', 'UNKNOWN') OR (state = 'CLAIMED' AND lease_expires_at < now()) ORDER BY updated_at FOR UPDATE SKIP LOCKED LIMIT 1) RETURNING ;Why the compare step is safe, and where it is not
The intake compare is a read followed by a decision, which is normally a race. It is safe here because the (tenant_id, operation_id) primary key serializes the insert and scope_hash is never updated, so the thing being compared cannot change underneath you. That is a property of this schema, not a general claim that read-compare-then-write is atomic.
The target-side delete needs its own conditional mutation. Where you own the store, make the epoch part of the statement:
DELETE FROM profile WHERE tenant_id = $1 AND subject_ref = $2 AND subject_epoch <= $3; -- the epoch approved in the operationMany third-party APIs offer no such condition. For those, you have two choices. You can freeze the subject so no new registration can reuse that identity during the operation, or you can accept and disclose the limitation: a re-created record in that vendor could be removed by a delayed delete. Pick one deliberately and write it down. Do not leave it to chance.
The mechanics of idempotency keys are covered in depth in our idempotency framework. For the tenant dimension, see multi-tenant database isolation patterns. A tenant-scoped key is one of those details that is easy to omit and very hard to retrofit.
Not sure every store with personal data has an owner?
An erasure workflow is only as complete as your data inventory. Gaps found now cost far less than gaps found during a request.
Talk to Developers.dev engineers about reviewing your data flows and backend architecture.
Contact UsVerified Acknowledgment, Ambiguous Timeouts, Terminal Failure and Uncertain Input
Key takeaway: Each connector must separate four outcomes that naive code collapses into "success" or "retry": confirmed absent, accepted but unconfirmed, unknown, and terminal failure.
Verified acknowledgment
A connector acknowledges completion only after it has observed the outcome. For a database that is a lookup by subject that returns no rows. For an asynchronous vendor job it is either the vendor's completion record or a later read that shows the data absent. If a vendor notifies you by webhook, treat that callback as an input to verify and deduplicate, not as a trusted fact. The receiving side has its own failure modes, covered in durable webhook receiver design.
Ambiguous timeouts: reconcile before you retry
A timeout on a delete call tells you nothing about whether the delete happened. Deleting again is often harmless, but not always: with no epoch condition, the retry could remove a record created after the first attempt. The rule is to reconcile first. Ask the system for the authoritative state, then decide. The general pattern is described in our guide to ambiguous timeouts in consequential API calls. The connector sketch below applies it. It is illustrative pseudo-code, not a specific SDK.
def run_task(task, connector): # 1. Reconcile first: what is true right now? observed = connector.verify(task.subject_ref, task.subject_epoch) if observed.kind == 'ABSENT': return finish(task, 'VERIFIED_ABSENT', observed.evidence) if observed.kind == 'INDETERMINATE': # unreachable, partial, stale return mark(task, 'UNKNOWN', reason='verify_indeterminate') # 2. Data is present: delete with a conditional, idempotent request. key = f'{task.tenant_id}:{task.operation_id}:{task.system_id}' try: resp = connector.delete(task.subject_ref, task.subject_epoch, idempotency_key=key) except Timeout: return mark(task, 'UNKNOWN', reason='delete_timeout') # do NOT assume success if resp.kind == 'ACCEPTED_ASYNC': return mark(task, 'REQUESTED') # reconciler verifies later if resp.kind == 'REFUSED_PERMANENT': return mark(task, 'FAILED_TERMINAL', reason=resp.code) if resp.kind == 'RETAINED_BY_POLICY': return mark(task, 'EXEMPT', reason=resp.code) # 3. A success response is not proof. Verify before closing. again = connector.verify(task.subject_ref, task.subject_epoch) if again.kind == 'ABSENT': return finish(task, 'VERIFIED_ABSENT', again.evidence) return mark(task, 'UNKNOWN', reason='delete_ok_but_unverified')Terminal failure needs a destination
If every failure goes back into a retry loop, hard failures become invisible. Define the terminal states up front: EXEMPT for data retained on an approved basis (with reason code and approver), and FAILED_TERMINAL for stores that cannot comply (for example, a vendor tool with no deletion capability). Both must surface to a human before the operation's due date, and both need to appear in whatever response goes back to the requester.
Uncertain input is not "not found"
Uncertainty enters at the edges, and a connector that treats it as an empty result will report a false success.
| Uncertain input | Wrong handling | Correct handling |
|---|---|---|
| Requester identity not verified | Start deletion because the email matches | Hold intake until verification completes. Deleting the wrong person's data is irreversible. |
| One email maps to two internal subjects | Delete the first match | Stop and escalate. Require disambiguation. |
| Store returns "not found" | Mark VERIFIED_ABSENT | Distinguish absent from never indexed, wrong shard, wrong region or lookup key mismatch. If the lookup path is unreliable, mark UNKNOWN. |
| Vendor returns 202 Accepted | Mark done | Mark REQUESTED and verify later. |
| Verify call times out | Assume absent, or assume present | Mark UNKNOWN. Retry verify within the deadline, then escalate. |
| Subject reference missing from a derived store | Skip the store | Mark the store as lacking a subject key. That is a design gap with an owner. |
Store-specific patterns
- Event logs. Options include key-based compaction with tombstones, retention expiry, or encrypting each subject's payloads under a per-subject key and destroying the key. See the Apache Kafka documentation for compaction semantics. Compaction is not immediate and needs keyed records. Crypto-shredding adds key management and requires a decision, with counsel, on whether key destruction satisfies your erasure obligation. If you outbox your events, the outbox pattern also puts personal data in a table you must include in the inventory.
- Search and cache. Delete by document key, then make reindex jobs read from a source that has already been cleaned. Otherwise the next rebuild reintroduces the data.
- Multi-step workflows. If a deletion spans services that must happen in an order (stop processing, delete dependents, delete identity mapping last), the compensation and ordering concerns look like the saga pattern, with one change: erasure usually has no compensation. You cannot un-delete. Design for forward recovery only.
Why This Fails in the Real World: Common Failure Patterns
Key takeaway: Capable teams fail at deletion because ownership, definition of done and derived data fall between teams. The scenarios below are illustrative composites, not reports of specific customer incidents.
Failure pattern 1: The resurrected record (illustrative)
A team deletes a user from the primary database and receives a success response. A nightly job rebuilds the search index from a data lake snapshot taken before the deletion. The next morning the person's profile is searchable again. Nobody was careless. The deletion service owned the primary database, the search team owned the index, and the data lake had no owner for personal data at all. Systemic cause: no inventory of derived stores, and "done" defined as the first store succeeding. Design response: a task per store, with reindex sources included in the inventory and a verification read against the index itself.
Failure pattern 2: The delayed retry that deletes the wrong thing (illustrative)
A deletion request times out against an internal service. A queue retries it twenty minutes later. In the meantime, the person registered a new account with the same email, a legitimate action. The retry deletes by email and removes the new account. Systemic cause: the retry was bound to a natural key rather than to the subject epoch it was approved for, and nothing distinguished the earlier request from the later, legitimate state. Design response: operation identity plus subject_epoch, conditional deletes where possible, and reconcile-before-retry.
Failure pattern 3: Accepted treated as completed (illustrative)
A marketing SaaS tool accepts a deletion call with a 202 response. The vendor's background job fails on a malformed record. The internal ticket was closed on the 202. Months later a data inventory review finds the profile still there. Systemic cause: asynchronous acceptance was recorded as completion, and there was no reconciler. Design response: a REQUESTED state, a later verification, and escalation if completion is not confirmed before the deadline.
Failure pattern 4: The restore that undoes the erasure (illustrative)
After an incident, operations restore a database from a backup taken before several deletions. The service comes back with the deleted people in it. The restore runbook had no step that re-applied deletions. Systemic cause: backup retention and erasure were handled by different teams with different runbooks. Design response: treat the ledger as a replay source. After any restore, re-run every verified operation whose subject data could be present in the restored copy. This is an engineering design that reduces the risk. How regulators view backups is a legal question, so ask counsel.
Why intelligent teams still fail
- Nobody owns the whole. Each service team is correct about its own store.
- "Done" is defined locally. An HTTP 200 is a local definition of done.
- Derived data is invisible. Embeddings, summaries, caches and analytics tables rarely carry a subject key.
- The happy path is the only tested path. Deletion paths for timeouts, vendors and restores are rarely exercised before a real request.
- The deadline is not a system input. A due date held in a spreadsheet cannot trigger escalation.
If your platform has accumulated several of these gaps, the usual remedy is a staged recovery. Begin with the data inventory and the ledger, then connect stores in order of risk. Our guide to post-launch triage for failing production systems describes how to sequence that kind of remediation without freezing feature work.
Go/No-Go Controls, Trade-offs and a Lower-Risk Rollout
Key takeaway: Readiness is a list of mandatory controls. Every one must be met. A summed score would let a strong area hide a missing control, and no document, including this one, certifies that your system is compliant.
Mandatory go/no-go controls
| # | Control | No-go if... |
|---|---|---|
| 1 | Data inventory lists every store, derived store and vendor that holds personal data, with an engineering owner and a privacy owner. | Any store has no owner, or the inventory has never been reconciled against real data flows. |
| 2 | Every store can be searched by an opaque subject reference, or has a documented lookup path. | Any store can only be searched by raw email or free text with no documented method. |
| 3 | Operation identity is durable and tenant-scoped, bound to an immutable payload hash. | A reused ID with a changed payload is accepted or merged. |
| 4 | Intake writes the operation, tasks and schedule atomically before acknowledging. | A crash between steps can lose or orphan a request. |
| 5 | Workers claim tasks atomically with leases. | Two workers can run the same task concurrently. |
| 6 | Deletes are target-side conditional where supported. Limitations are disclosed where not. | A delayed retry could delete a legitimately re-created record, and nobody has accepted that risk. |
| 7 | Connectors verify absence by reading the system. A delete response is never accepted as proof. | Any connector closes a task on the delete response alone. |
| 8 | UNKNOWN, EXEMPT and FAILED_TERMINAL states exist, carry reason codes and have owners and deadlines. | Failures loop silently, or are dropped. |
| 9 | Restore procedures replay completed erasures. | A restore can reintroduce deleted subjects without detection. |
| 10 | Vendor deletion paths have documented evidence (receipt, API verification or contract path). | A vendor holds personal data and there is no way to confirm deletion. |
| 11 | The ledger stores minimal data, has its own access controls and a decided retention period. | The ledger holds raw identifiers with broad access. |
| 12 | Counsel has reviewed scope, exemptions and response wording for your jurisdictions and role. | The workflow's definitions of "deleted" and "exempt" were set by engineering alone. |
Trade-offs you should decide on purpose
| Decision | Option A | Option B | What it depends on |
|---|---|---|---|
| Hard delete vs. anonymize | Hard delete: cleanest, can break referential integrity and reports. | Anonymize: preserves aggregates, but must be irreversible and reviewed for re-identification risk. | Downstream dependencies and legal interpretation of anonymization. |
| Event log deletion | Compaction with tombstones: simple, needs keys, not immediate. | Per-subject encryption keys (crypto-shredding): immediate effect, adds key management. | Topic design, key infrastructure, and whether counsel accepts key destruction as erasure. |
| Orchestration | Central ledger: one view, evidence and deadlines. | Choreographed events only: loosely coupled, no completion signal. | Number of stores and how much audit evidence you need. |
| Freeze behavior | Freeze subject during operation: blocks accidental re-collection. | No freeze: no user impact, but conditional deletes must carry the safety burden. | Vendor condition support and product tolerance for brief lockout. |
A lower-risk rollout path
- Inventory first. Build the store list, owners and subject lookup paths. This alone often surfaces unowned derived stores.
- Ship the ledger with two or three connectors. Start with the primary database, the search index and one vendor. Run in observe-and-verify mode: read-only verification against real stores before any automated delete.
- Enable automated deletion per connector once its verification has proven reliable on real data. Keep the rest on the manual runbook, tracked in the same ledger.
- Add the hard cases: event logs, warehouse and restore replay.
- Rehearse. Create test subjects across all stores, run erasures, and inject timeouts, vendor failures and a restore. Treat a failed rehearsal as a cheap bug, not an incident.
2026 Update: more derived data, same fundamentals
Through 2026, the number of places where personal data can be copied has grown, mostly through AI features. Retrieval indexes, embeddings, prompt and response logs, and agent memory are all derived stores, and they rarely carry a subject key unless you design for it. Whether personal data embedded in a trained model can be removed is a harder, still-unsettled question that varies by approach, and you should scope it explicitly with counsel rather than assume it away. The engineering fundamentals do not change with the technology. Know every store. Give each a connector. Verify absence. Never treat unknown as success. See production-ready RAG architecture, persistent memory for AI agents and LLM observability for where these stores appear in practice.
Is your "delete my data" path tested, or just assumed?
The first real request is a poor time to learn that a vendor job fails or a restore brings data back.
Developers.dev backend and DevOps engineers can help you design and rehearse an erasure workflow.
Request a ConsultationConclusion: What to Do Next
Deletion fails in distributed systems when completion is assumed instead of verified. The following actions move you from assumption to evidence, in order of leverage:
- Build the inventory this quarter. List every store, derived store and vendor that holds personal data. Assign an engineering owner and a privacy owner to each. Treat any store you cannot assign as your first finding.
- Define "done" as "verified absent". Change connector contracts so no task closes on a delete response. Add UNKNOWN, EXEMPT and FAILED_TERMINAL states with reason codes and deadlines.
- Make operations durable and tenant-safe. Use a tenant-scoped operation ID bound to a payload hash, atomic intake, leased claims and a subject epoch, so retries cannot delete something they were never approved to delete.
- Rehearse the ugly paths. Run a test erasure across all stores with injected timeouts, an asynchronous vendor failure and a backup restore. Fix what the rehearsal exposes before a real request exposes it.
- Get counsel to review scope and definitions. Have them confirm your obligations, exemptions and the wording of what you tell requesters, and then encode those decisions as reason codes, not tribal knowledge.
About the authors
Developers.dev is an offshore software development and staff augmentation company founded in 2007, with more than 1,000 in-house engineers delivering backend, cloud, DevOps and security work for clients in the USA, EMEA and Australia. The company lists CMMI Level 5, SOC 2 and ISO 27001 among its accreditations. Those describe Developers.dev's own delivery processes. They do not certify any particular client system or this design as compliant. This article was reviewed by the Developers.dev Expert Team. It offers engineering guidance only, not legal advice.
Frequently Asked Questions
What is the safest way to delete personal data across microservices?
Treat deletion as a durable operation with one task per system holding personal data. Each connector should delete, then verify absence by reading the system, and record evidence. Mark a task done only after verification. Keep tasks that are unconfirmed, exempt or failed in explicit states with owners and deadlines.
- Use a tenant-scoped operation ID bound to an immutable payload hash.
- Create the operation, tasks and schedule in one transaction.
- Claim tasks atomically so two workers never run the same one.
Is publishing a UserDeleted event enough to erase data downstream?
No. An event shows that something was published, not that every subscriber processed it or that its data is gone. Subscribers may crash, lag, or never have been subscribed. Use events to trigger work, but track completion per system in a ledger and verify each outcome. A publish that is not tied to a durable task can also be lost; the outbox pattern addresses that part.
Should I retry a deletion request that timed out?
Reconcile first. A timeout means the outcome is unknown, so query the system for its current state. If the data is absent, record verified completion. If it is present, retry with an idempotency key and, where the target supports it, a condition such as a subject epoch. This ensures a delayed retry cannot remove a legitimately re-created record.
How do I handle deletion requests across backups?
A common engineering approach is to let backups expire on a documented retention schedule and to replay completed erasures after any restore, so deleted subjects do not silently return. Whether and how backups must be addressed depends on your jurisdiction and role, so confirm the approach with counsel and document it.
Are hashed emails or internal user IDs safe to put in logs and ledgers?
Not automatically. Under GDPR Recital 26, pseudonymised data that can be attributed to a person with additional information remains information on an identifiable person. Use opaque internal references where you can, keep the reference-to-identity mapping tightly controlled, minimize what the ledger stores, and decide its retention with counsel.
How long do we have to respond to an erasure request?
It depends on the law that applies to you. As examples, GDPR Article 12(3) requires information on action taken within one month of receipt, extendable by two further months where necessary, with notice and reasons. The California Attorney General describes a 45-calendar-day response period under the CCPA, extendable by 45 days if the consumer is notified. Your workflow should store received and due timestamps and record extensions and their reasons. Have counsel confirm which deadlines apply.
Can I use crypto-shredding instead of deleting records from an event log?
It is a recognized design option. You encrypt each subject's data under its own key and destroy the key. It gives immediate effect on log-like stores, but adds key management, key backup concerns and a legal question about whether key destruction counts as erasure for your obligations. Evaluate it with counsel and compare it against compaction with tombstones or retention expiry.
Does this article make my system compliant?
No. The go/no-go controls are an engineering readiness checklist. Every control must be met, but meeting them does not certify compliance. Your legal obligations depend on your role, jurisdictions and applicable exemptions, and they should be confirmed with qualified counsel.
Need a deletion workflow you can actually evidence?
Unowned data stores, unverified vendor jobs and untested restores are cheaper to fix before a request arrives than after it.
Work with Developers.dev backend, DevOps and security engineers to inventory your data flows and build verified erasure into your architecture.
Contact UsDevsecops Services
This guide is designed for engineering leaders who want to plan a practical implementation. Use the related Developers.dev path to compare delivery options, implementation fit, risk, and practical next steps.
Reviewed by the Experts team
This guide is reviewed for clarity, technical and operational relevance, service alignment, and a useful next step. Verified by our SEO team for clear search presentation.
Reviewed by the Experts team. Verified by our SEO team. Validate legal, security, data, budget, and operational requirements with the relevant stakeholders before rollout.

