Scenario & Constraints
Design a 50M-user multi-tenant SaaS data platform serving EU users. A user clicks "delete my account" — you must erase their PII from everywhere within 30 days (GDPR Article 17). But that person's data is scattered across 40 microservice OLTP databases, a Snowflake warehouse, Redis caches, Kafka event streams, Elasticsearch indexes, and every DB backup from the last 90 days.
Why this is the most counter-intuitive System Design problem: every instinct of distributed systems fights compliance. Append-only, immutable logs, multi-replica, event sourcing — all built so "data is never lost." GDPR wants exactly the opposite: "make specific data vanish permanently, and prove it." Other constraints: deletion must be verifiable, you must not over-delete legally-retained data (tax / anti-fraud have minimum retention), anonymized analytics data must not be re-identified, and every PII access must be logged.
High-Level Architecture
graph TD
U[User DSAR
erase / export / withdraw consent] --> ORCH[Privacy Orchestrator
DSAR Orchestrator]
CAT[(PII Data Catalog
Registry)] -.locate data.-> ORCH
ORCH -->|fan-out delete| SVC1[Service A]
ORCH -->|fan-out delete| SVC2[Service B ...N]
ORCH -->|destroy key| KV[[Key Vault
KMS / crypto-shred]]
KV -.holds per-user DEK.-> DB[(Encrypted store)]
ORCH -->|log every step| AUD[[Audit Log
append-only hash chain]]
CG[Consent Gate] -.intercept each op.-> SVC1
CG -.intercept each op.-> SVC2
classDef ctrl fill:#1a2530,stroke:#64c8ff,color:#e8eef5
classDef store fill:#2a1530,stroke:#ff7ab6,color:#e8eef5
classDef sec fill:#1a1a30,stroke:#ffb450,color:#e8eef5
class U,ORCH,CG ctrl
class DB,CAT store
class KV,AUD sec
The orchestrator is the "transaction coordinator of deletion"; the catalog answers "where is the PII"; the key vault reduces deletion to deleting one key; the audit chain proves "we actually did it"
Four core capabilities: ① orchestrator fans one deletion out into dozens of subtasks and tracks completion (essentially a long transaction / Saga); ② data catalog solves the deadliest problem — "we don't know where PII is scattered"; ③ key vault + crypto-shredding turns the impossible task of "physically deleting from backups" into "delete one key"; ④ consent gate checks before every operation whether the user authorized this purpose.
Key Technical Points
1. Right to Erasure: Crypto-shredding vs Physical Delete vs Tombstone
Principle: GDPR erasure doesn't require physical wiping — only that data become permanently unrecoverable. This opens a shortcut: crypto-shredding — give each user a unique data encryption key (DEK), encrypt all their PII with it, and to delete, destroy only that DEK. Ciphertext scattered across 40 stores and 90 backups instantly turns into undecryptable garbage — no need to hunt down every copy. The European Data Protection Board (formerly the Article 29 Working Party) recognizes cryptographic erasure as a valid deletion method.
Trade-off:
- Physical DELETE / cascade: ✅ cleanest semantics, truly gone; ❌ must traverse every system (incl. backups — you can't UPDATE a row in a cold backup), cross-service FKs, slow, and can't verify "did we miss a replica."
- Tombstone (soft-delete + flag): ✅ simple, auditable, can cascade delete events; ❌ PII still physically exists, just "hidden" — strictly does not satisfy Article 17, only a first step to trigger real deletion.
- Crypto-shredding: ✅ one key-delete covers all copies incl. backups, O(1) fast, verifiable (key gone = deleted); ❌ key management becomes a single point (KMS down = nothing decrypts), encryption CPU cost, guarantee void if key was ever leaked, schema must be designed around per-user keys from the start.
# Crypto-shredding: deletion degrades to deleting one key (pseudo-code)
def write_pii(user_id, field, value):
dek = kms.get_or_create_key(f"user:{user_id}") # per-user DEK
db.put(user_id, field, aes_encrypt(dek, value)) # store ciphertext
def read_pii(user_id, field):
dek = kms.get_key(f"user:{user_id}") # key gone -> raise -> treated as deleted
return aes_decrypt(dek, db.get(user_id, field))
def erase_user(user_id):
kms.destroy_key(f"user:{user_id}") # only action: ciphertext in backups/warehouse/Kafka all voided
audit.append("ERASE", user_id, proof=kms.key_destroyed_receipt(user_id))
Real cases:
- Envelope encryption is the industry-standard pattern: AWS KMS / GCP Cloud KMS treat per-entity DEK wrapped by KEK as first-class; crypto-shred = delete the KEK.
- Kafka community: topics are append-only and can't delete a single record in place, so crypto-shredding is the mainstream way to do GDPR deletion on Kafka (each message's PII encrypted with a per-key DEK; delete the key to invalidate).
2. Anonymization: Pseudonymization vs Anonymization (re-identification is the killer)
Principle: warehouses need analytics, test environments need realistic data — but neither can use raw PII. The two paths have completely different legal consequences: pseudonymization — replace names with tokens but keep a reversible mapping table — data remains PII, still fully under GDPR (leak the mapping and it's over); anonymization — irreversibly sever any link to the individual — is what escapes GDPR. The hard part is "irreversible" is nearly unachievable: dropping the name is far from enough, and combinations of quasi-identifiers re-identify. Classic result: ZIP + birth date + gender together uniquely locate the vast majority of the US population.
Trade-off (privacy vs utility, one at the other's expense):
- k-anonymity: make each record indistinguishable from at least k-1 others on quasi-identifiers (generalize age to ranges, truncate ZIP). ✅ intuitive; ❌ fails the homogeneity attack (if all k share the same sensitive value, it still leaks).
- l-diversity / t-closeness: on top of k-anonymity, require enough diversity of the sensitive attribute per group. ✅ patches homogeneity; ❌ heavier generalization, worse utility.
- Differential privacy (DP): inject calibrated noise into query results, quantify privacy with an ε budget. ✅ only method with rigorous mathematical guarantees, robust to background knowledge; ❌ noise sacrifices accuracy, ε budget depletes with queries, engineering complexity.
Real cases:
- Apple / Google: use local differential privacy for telemetry and keyboard stats (noise added on-device before data leaves), collecting trends without touching raw individual values.
- US Census Bureau: the 2020 census was the first full-scale production use of differential privacy for statistical releases — DP's largest deployment.
- GDPR practice: pseudonymization is explicitly listed as a "security measure" (Article 32), not an escape hatch — many teams wrongly assume tokenized data can be used freely for analytics. This is a high-frequency compliance failure.
3. Audit Trail: Prove Tamper-Evidently "Who Touched the PII"
Principle: compliance isn't just "do it right," it's being able to prove it. The audit log records every PII access / modify / delete / export — who, when, why, which record. The requirement is append-only + tamper-evident: even a DBA with root can't silently rewrite history. The core technique is the hash chain: each entry carries "the hash of the previous entry," forming a chain — altering any middle entry breaks all subsequent hashes. Same idea as blockchain (see Day 36) but with no consensus needed; a single-node Merkle/hash chain suffices.
# Tamper-evident audit chain (pseudo-code)
def append_audit(actor, action, subject_id, purpose):
prev = store.last().hash if store.last() else GENESIS
entry = {actor, action, subject_id, purpose, ts, prev_hash: prev}
entry.hash = sha256(canonical(entry)) # includes prev_hash -> chained
store.append(entry) # WORM / append-only media
# Verify: recompute each hash from genesis; any mismatch -> tampered
Trade-off: the hash chain is tamper-resistant but append-only (can't change = can't delete, which clashes with the right to erasure — see pitfalls); WORM storage (S3 Object Lock) guarantees immutability via media — simple but costly, protection ends when retention expires; external anchoring (periodically notarize/anchor the chain head hash) defends against an insider rebuilding the whole chain, but adds an external dependency. Most teams: hash chain + WORM cold storage + periodic offline snapshots — sufficient and affordable.
Real cases:
- AWS CloudTrail provides log file integrity validation: hashes + signs delivered logs so you can later prove they weren't altered.
- Finance/healthcare compliance (SOX, HIPAA, PCI-DSS) broadly require non-repudiable audit; WORM + hash chain is standard.
- Databases: AWS QLDB / SQL Server Ledger bake Merkleized immutable history into the engine, offering cryptographically verifiable audit natively.
4. Consent Management: Treat "What the User Authorized" as the Source of Truth
Principle: GDPR's core is purpose limitation — an address collected for "shipping" can't be reused for "targeted ads." So consent isn't a checkbox ticked once at signup; it's a source of truth over (user × purpose × state × time) that must propagate in real time to every downstream processing point: before the marketing service acts, it asks "did this user consent to marketing?" — a gate in the dataflow, like a policy / feature flag. Withdrawing consent must cascade like deletion: not only stop future processing, but clean up, by purpose, data already flowed into downstream pipelines.
Trade-off:
- Centralized consent service (synchronous query): ✅ always the latest state, single truth; ❌ an extra RPC on every operation, a hot-path dependency and single point.
- Consent travels with data (consent-as-metadata): pack the consent scope into the event / record itself. ✅ local decisions downstream, no extra calls; ❌ after withdrawal, "in-flight data" still carries the old consent → needs a separate invalidation broadcast.
- Pure event broadcast: consent changes go to Kafka, each service materializes locally. ✅ decoupled, low latency; ❌ eventually consistent, with a window from withdrawal to effect (compliance requires proving the window is short enough).
Real cases:
- IAB TCF (Transparency & Consent Framework): the digital-advertising standard consent protocol, passing an encoded consent string among ad-chain parties — exactly an industrial implementation of "consent travels with data."
- CDP / event platforms (e.g. Segment-style): route events at the ingestion point by the user's consented purposes; unauthorized purposes never get stored — purpose limitation at the source.
- Cookie consent banners: the front-end CMP is the user-facing end of this system; the backend is the source of truth and enforcement point.
Scaling & Optimization
- Automate the data catalog: manually maintaining "where PII is" inevitably drifts. Use schema scanning + classifiers (regex/ML to spot emails, national IDs, credit cards) to auto-tag, and the orchestrator fans out accordingly, avoiding misses.
- Deletion as Saga: deleting across 40 services is a long transaction — orchestrator + per-service idempotent delete APIs + retries + dead-letter; any failed step can be compensated/replayed; mark the user done only after verifying "all subtasks done."
- Legal-retention exemptions: before deleting, pass through a "retention policy engine" — tax invoices and anti-fraud records have minimum legal retention; anonymize rather than delete those fields, deleting only the deletable parts.
- Data-localization interplay: EU PII keys and ciphertext both stay in the EU region (see Day 42 multi-region); the crypto-shred key vault must also be region-isolated.
Common Pitfalls + Interview Questions
1. How do you delete from backups? This is what breaks physical-delete plans — you can't UPDATE a cold backup. Standard answers: either crypto-shred (delete the key, backup ciphertext is voided); or accept "backups roll off by their retention window, and meanwhile use access control + a record that 'this user requested deletion; re-run deletion if a backup is restored.'" Interviewers love probing this.
2. Audit logs are immutable — so how do you delete the PII inside them? Paradox: audit wants append-only, erasure wants removal. Solution: store references, not plaintext PII in the audit (a pseudonym/hash of user_id + purpose, not the raw name/email), or crypto-shred the PII in the audit too. Never write plaintext PII into an undeletable chain.
3. Pseudonymization ≠ anonymization. Many teams treat tokenized data as "anonymized" and use it freely for analytics — as long as the mapping table exists, it's still PII legally, and deletion requests must cover it. This is the most common audit finding.
- A user requests deletion; data lives in 40 services + warehouse + 90-day backups. How do you design end-to-end deletion and prove it's clean?
- The crypto-shred key vault is itself a single point — what if KMS data is corrupted? How to balance "deletable" against "don't lock a live user's data forever"?
- Why is name-stripped data still re-identifiable? Give a quasi-identifier attack, and how k-anonymity is broken by homogeneity.
- A user withdrew ad consent, but events already entered downstream Kafka pipelines. How do you ensure downstream stops using it for ads, and prove it?
- Append-only audit chains directly conflict with the right to erasure — how do you satisfy both?
Deep Resources
- Designing Data-Intensive Applications, Ch 12 (Kleppmann): closes with the ethics of data, tracking, and the systemic difficulty of "being forgotten."
- GDPR Articles 17 & 25: the right to erasure and "Privacy by Design / by Default" — the statutory starting point for compliance architecture.
- Forgotten @ Scale (IBM, arXiv:1910.13784): design patterns for implementing erasure at scale (locate / delete / prove).
- Sweeney, k-Anonymity (2002): the foundational paper on quasi-identifiers and re-identification; pair with differential privacy (Dwork) for the mathematical boundary of privacy.
- AWS KMS / Kafka crypto-shredding docs: engineering practice of envelope encryption and per-key deletion.
Going Deeper
Crypto-shredding turns "delete data" into "delete a key" — but if an attacker copied the ciphertext before you deleted the key, can you still claim "deleted"?
Not fully. Crypto-shred guarantees "henceforth no one (including you) can decrypt again" — it defends against parties who keep holding the ciphertext (backups, warehouses, an ex-employee's old snapshot) decrypting in the future. But it cannot undo a leak that already happened — if an attacker decrypted and exported plaintext while the key was valid, deleting the key does nothing. This is crypto-shredding's boundary: it's a deletion mechanism, not a breach-remediation one. So it satisfies Article 17 (unrecoverable henceforth), but doesn't change the obligation to report a breach under Articles 33/34. The two are orthogonal: deletion guarantees "future inaccessibility"; a breach concerns "past accessibility."
You generate a DEK per user for crypto-shredding. 50M users = 50M keys — can KMS handle it? Where's the bottleneck, and how do you get around it?
Calling KMS to decrypt 50M DEKs on every read/write would melt KMS. In practice, use two-layer envelope + caching: KMS holds only a few KEKs (master keys); each user's DEK is encrypted by a KEK and stored alongside the ciphertext (wrapped DEK); reads cache the unwrapped DEK locally (a few-second TTL) to avoid hitting KMS each time. Deletion destroys "the ability to unwrap that user's DEK": with a shared KEK you'd have to delete wrapped DEKs scattered in backups (back to the old problem); the truly clean design is a per-user KEK — deletion = delete that one KEK in KMS, O(1) and no need to chase backups. The cost is KMS must handle the lifecycle of tens of millions of keys — precisely the scale problem cloud KMS's "keys as resources" design solves. The insight: optimize "number of keys" (via layering) and "KMS call frequency" (via caching) as separate dimensions.
Audit logs must be both "immutable" and "able to delete PII inside." Beyond "don't store plaintext," is there another architectural solution?
Yes — layered decoupling: split the audit into an immutable "event skeleton" + a deletable "PII payload." The skeleton (who, when, what type of op, a reference hash to the payload) goes into the hash chain and is never changeable — it proves "an access to user X occurred"; the PII payload (the actual values) lives separately in a crypto-shreddable store. On deletion, destroy the payload key: the skeleton stays intact, the chain is unbroken, tampering is still detectable, but the payload is unreadable — "audit integrity" is guaranteed by the skeleton, "right to erasure" by key destruction, each in its place. Going further you could use a redactable blockchain (chameleon hash lets an authorized party edit an entry without breaking chain verification), but complexity spikes; for most cases "skeleton + payload separation" suffices. Core lesson: when two constraints conflict, it's often not either/or, but splitting the object into two parts under different constraints.
"Deletion as Saga" spans 40 services. If the 37th can never delete (say a deprecated, unmaintained system still holds the user's data), does the whole request count as success or failure? What do you do for compliance?
Compliance-wise it cannot count as success — Article 17 requires deleting "all" copies; miss one and you haven't complied. But "zombie systems" really exist. Correct handling has two layers: technically, the orchestrator must mark that subtask failed and keep retrying + alerting, never silently swallow it (the most dangerous outcome is "39/40 succeeded, report done" — the 40th's PII becomes a compliance landmine); governance-wise, this exposes an incomplete catalog / shadow copies — the root cause is that when that service copied PII, it wasn't registered in the catalog. This is also crypto-shredding's strategic value: if all 40 services store ciphertext under the same per-user key, you don't even need each service to cooperate — delete the central key and the zombie's ciphertext is voided automatically. It compresses the AND-problem "N distributed deletes must all succeed" into a single "delete one key" operation.
Differential privacy gives mathematical guarantees — so why are tons of "anonymized" datasets still re-identified? What's the real barrier to DP adoption?
Because most "anonymization" isn't DP at all — it's de-identification + k-anonymity, i.e. syntactic anonymity, with no guarantee against an attacker holding external auxiliary data. Classic failure: the Netflix Prize dataset was re-identified by cross-referencing public IMDb ratings. These datasets "removed names and did k-anonymity," but quasi-identifiers + external knowledge still locate individuals; DP's power is that it holds against any background knowledge. So why hasn't DP spread? Three real barriers: ① utility loss — noise makes fine-grained / small-sample queries nearly unusable; ② the ε budget depletes — the same data queried repeatedly leaks a bit each time, and budget management is very hard when many teams share data; ③ what ε to pick has no universal answer — too big and there's no protection, too small and there's no data. So DP lands well in aggregate statistical releases (census, telemetry) but is still hard to substitute for row-level analytics. Understanding this gap matters more than memorizing "DP is safe."