</>longpham.tech
All posts
·8 min read

Privacy Is an Architecture Constraint

'Delete my account' is genuinely hard because personal data is scattered across tables — and some of it you're legally required to keep. A runnable lab where a declarative data map drives GDPR rights end to end.

#privacy#architecture#gdpr#data

Most engineers meet privacy law as paperwork, then discover it was an architecture decision all along. The moment a user clicks "delete my account", you learn that their personal data is scattered across a dozen tables, that some of those rows are referenced by records you can't delete, and that a few of them you're legally required to keep. None of that is solvable at one endpoint. It's a constraint on how you model and link data — the kind many engineers overlook until it's expensive.

Runnable companion: privacy-by-design-reference on GitHub. make up && make seed && make access SUBJECT=1 && make erasure SUBJECT=1 && make verify SUBJECT=1 runs the whole flow.

Why "delete my account" is hard

Personal data spreads — across six tables in this small lab, across services, caches, search indexes and backups in a real one. "Erase this person" then hits three problems at once:

  1. Coverage — you must touch every place their data lives, and not miss one. Each new table is a fresh chance to forget.
  2. Referential integrity — some rows are referenced by records you must keep, so you can't just delete them.
  3. Legal holds — financial records, consent evidence and audit trails must be retained, so "erase everything" is actually the wrong answer.

A growing pile of hand-written DELETE statements fails all three over time.

The data map is the architecture

The fix is to stop treating the PII inventory as tribal knowledge and make it an explicit, declarative artifact that the code is driven by. One YAML file declares, per table, which column links a row to a subject, which columns are PII, and what each right does:

TableErasure policyWhy
usersanonymizekeep the row (orders reference it), scrub identifiers
addressesdeletecontact PII, no reason to retain
support_ticketsdeletefree text may embed PII; also auto-purged after 24 months
ordersretainfinancial record — legal obligation (~10 years)
consentsretainproof of lawful basis
audit_logretainrecord of processing (incl. the erasure itself)

Every script — access, erasure, verification, retention — iterates this map. Coverage stops being a matter of memory: add a table to the map and the rights machinery covers it. The map is simultaneously a compliance document a DPO can read and the executable policy, so the two can't drift.

Erasure is a cascade, not a DELETE

The right to be forgotten runs as a single transaction that does three different things:

  • delete child rows with no retention need (addresses, tickets),
  • anonymize the users row — kept, because retained orders reference it, but its identifiers overwritten,
  • retain the financial, consent and audit records,

and then writes the erasure event itself to the audit log. Deletes run before the parent is anonymized so foreign keys never break.

The verifiable result

A right you can't verify, you can't defend. After erasure, a verification pass scans every mapped PII column and proves the outcome — while showing what was legitimately kept:

  [PASS] users            0 PII value(s) not anonymized (expected 0)
  [PASS] addresses        0 row(s) remain (expected 0)
  [PASS] support_tickets  0 row(s) remain (expected 0)
  [KEEP] orders           1 row(s) retained (legal_obligation)
  [KEEP] consents         3 row(s) retained (legal_obligation)
  [KEEP] audit_log        1 row(s) retained (legal_obligation)

RESULT: PASS — no PII remains; only legally-retained records are kept.

That's a complete, defensible answer to a regulator: the identifiers are gone, the financial record survives de-identified, and the audit log records that the erasure happened.

The one modelling move that makes it work

users is anonymized, not deleted. That single choice resolves the integrity-versus-erasure tension: the orders you must keep retain a valid foreign key to a users row that no longer identifies anyone. The person is erased; the accounting record remains, de-identified. Get this wrong and you get either orphaned foreign keys or illegally deleted financial data.

What I'd say in an interview

  • Data-subject rights are a cross-cutting architecture constraint, not a CRUD endpoint — design for them up front.
  • Make the PII map an explicit artifact and drive behaviour from it, so coverage is structural, not remembered.
  • Erasure is a policy cascade — delete / anonymize / retain — that respects foreign keys and legal holds; anonymizing the subject row is what keeps retained records valid.
  • Rights must be verifiable and auditable: prove the erasure, and record that you did it.