In 2024, global data privacy fines exceeded 4.5 billion euros. Meta alone was fined 1.2 billion euros for transferring EU user data to the US without adequate safeguards. Amazon, Google, TikTok, and hundreds of smaller companies faced significant penalties. The message is clear: data privacy is not optional, and "we'll fix it later" is not a strategy.
For developers and architects, privacy compliance is fundamentally an engineering problem. Lawyers define what the regulations require; engineers build systems that satisfy those requirements. This guide focuses on the technical implementation of privacy-compliant systems, covering architecture patterns, code examples, and infrastructure configurations that satisfy GDPR and similar regulations.
Chapter 1: Understanding the Regulatory Landscape
GDPR Core Principles (Technical Perspective)
The General Data Protection Regulation defines six principles that directly impact how you architect software:
1. Lawfulness, Fairness, and Transparency: You must have a legal basis for processing data (consent, contract, legitimate interest). Your system must record why each piece of data was collected and under which legal basis.
2. Purpose Limitation: Data collected for one purpose cannot be used for another without additional consent. Your database schema must track the purpose for each data point.
3. Data Minimization: Collect only the data you actually need. Every field in your registration form, every cookie you set, every analytics event you track must have a documented purpose. If you can achieve the same result with less data, you must use less data.
4. Accuracy: Keep personal data accurate and up to date. Provide users with the ability to correct their data. Implement validation at the point of collection.
5. Storage Limitation: Don't keep data longer than necessary. Define retention periods for every category of data and implement automated deletion.
6. Integrity and Confidentiality: Protect data with appropriate technical measures. Encryption, access controls, audit logging, and secure transmission are mandatory, not optional.
Key Rights You Must Implement
GDPR grants individuals specific rights that require technical implementation:
- Right of Access (Article 15): Users can request a copy of all data you hold about them. You need a data export system.
- Right to Rectification (Article 16): Users can request corrections. Your system must support updating personal data across all systems.
- Right to Erasure (Article 17): The "right to be forgotten." Users can request deletion of all their data. This is the hardest to implement technically.
- Right to Data Portability (Article 20): Users can request their data in a machine-readable format (JSON, CSV) to transfer to another service.
- Right to Object (Article 21): Users can object to processing based on legitimate interest or direct marketing.
Chapter 2: Privacy-by-Design Architecture
Data Classification System
Before you can protect data appropriately, you must classify it. Different categories of data require different levels of protection.
// data-classification.ts — Central data classification registry
export enum DataCategory {
PUBLIC = 'public', // No restrictions
INTERNAL = 'internal', // Business data, not personal
PERSONAL = 'personal', // PII: name, email, phone
SENSITIVE = 'sensitive', // Special category: health, religion, politics
FINANCIAL = 'financial', // Payment data, bank accounts
CREDENTIALS = 'credentials' // Passwords, API keys, tokens
}
export enum RetentionPeriod {
SESSION = 'session', // Delete when session ends
THIRTY_DAYS = '30d', // Logs, temporary data
ONE_YEAR = '1y', // Transaction records
THREE_YEARS = '3y', // Financial records
SEVEN_YEARS = '7y', // Tax-related financial data
INDEFINITE = 'indefinite' // Only with explicit consent
}
export interface DataFieldDefinition {
field: string
category: DataCategory
purpose: string
legalBasis: 'consent' | 'contract' | 'legal_obligation' | 'legitimate_interest'
retention: RetentionPeriod
encrypted: boolean
pseudonymizable: boolean
exportable: boolean
erasable: boolean
}
// Define every personal data field in your system
export const dataRegistry: DataFieldDefinition[] = [
{
field: 'user.email',
category: DataCategory.PERSONAL,
purpose: 'Account identification and communication',
legalBasis: 'contract',
retention: RetentionPeriod.THREE_YEARS,
encrypted: true,
pseudonymizable: true,
exportable: true,
erasable: true,
},
{
field: 'user.name',
category: DataCategory.PERSONAL,
purpose: 'Personalization and communication',
legalBasis: 'contract',
retention: RetentionPeriod.THREE_YEARS,
encrypted: false,
pseudonymizable: true,
exportable: true,
erasable: true,
},
{
field: 'user.ip_address',
category: DataCategory.PERSONAL,
purpose: 'Security and fraud prevention',
legalBasis: 'legitimate_interest',
retention: RetentionPeriod.THIRTY_DAYS,
encrypted: false,
pseudonymizable: true,
exportable: true,
erasable: true,
},
{
field: 'payment.card_last_four',
category: DataCategory.FINANCIAL,
purpose: 'Payment identification',
legalBasis: 'contract',
retention: RetentionPeriod.SEVEN_YEARS,
encrypted: true,
pseudonymizable: false,
exportable: true,
erasable: false, // Legal obligation to retain
},
]
Consent Management System
Consent is not a simple boolean. GDPR requires that consent be: freely given, specific (per purpose), informed (user understands what they're agreeing to), unambiguous (explicit opt-in, not pre-ticked boxes), and withdrawable (as easy to withdraw as to give).
// consent-manager.ts
export interface ConsentRecord {
id: string
userId: string
purpose: string
granted: boolean
grantedAt: Date | null
withdrawnAt: Date | null
version: string // Version of privacy policy
ipAddress: string // IP at time of consent
userAgent: string // Browser at time of consent
method: 'explicit_click' | 'api' | 'paper'
}
export interface ConsentPurpose {
id: string
name: string
description: string
required: boolean // Is this needed for service to work?
defaultState: boolean // Must be false for optional purposes
dataCategories: string[] // What data this consent covers
}
// Define all consent purposes
export const consentPurposes: ConsentPurpose[] = [
{
id: 'essential',
name: 'Essential Service',
description: 'Data processing necessary to provide the service you requested',
required: true,
defaultState: true,
dataCategories: ['account_data', 'authentication'],
},
{
id: 'analytics',
name: 'Analytics',
description: 'Anonymous usage analytics to improve our service',
required: false,
defaultState: false, // Must be opt-in
dataCategories: ['usage_data', 'device_info'],
},
{
id: 'marketing',
name: 'Marketing Communications',
description: 'Promotional emails and product updates',
required: false,
defaultState: false,
dataCategories: ['email', 'preferences'],
},
]
class ConsentManager {
// Record consent with full audit trail
async grantConsent(
userId: string,
purposeId: string,
context: { ip: string; userAgent: string }
): Promise<ConsentRecord> {
const record: ConsentRecord = {
id: crypto.randomUUID(),
userId,
purpose: purposeId,
granted: true,
grantedAt: new Date(),
withdrawnAt: null,
version: await this.getCurrentPolicyVersion(),
ipAddress: context.ip,
userAgent: context.userAgent,
method: 'explicit_click',
}
// Store in append-only consent log (never delete consent records)
await this.consentLog.insert(record)
// Update active consent cache
await this.activeConsents.set(userId, purposeId, true)
return record
}
// Withdraw consent (creates new record, doesn't delete old one)
async withdrawConsent(userId: string, purposeId: string): Promise<void> {
const record: ConsentRecord = {
id: crypto.randomUUID(),
userId,
purpose: purposeId,
granted: false,
grantedAt: null,
withdrawnAt: new Date(),
version: await this.getCurrentPolicyVersion(),
ipAddress: '',
userAgent: '',
method: 'api',
}
await this.consentLog.insert(record)
await this.activeConsents.set(userId, purposeId, false)
// Trigger data processing stop for this purpose
await this.stopProcessing(userId, purposeId)
}
// Check if user has active consent for a purpose
async hasConsent(userId: string, purposeId: string): Promise<boolean> {
return await this.activeConsents.get(userId, purposeId) ?? false
}
}
Chapter 3: Implementing the Right to Erasure
The right to erasure ("right to be forgotten") is the most technically challenging GDPR requirement. When a user requests deletion, you must: delete their personal data from your primary database, delete their data from backups (or ensure it's excluded during restore), delete their data from third-party systems (analytics, CRM, email providers), delete their data from search engine caches (if applicable), retain records you're legally required to keep (invoices, tax records), and document the entire process.
// erasure-service.ts
interface ErasureRequest {
id: string
userId: string
requestedAt: Date
status: 'pending' | 'processing' | 'completed' | 'partial' | 'failed'
completedAt: Date | null
retainedData: RetainedDataRecord[]
deletionLog: DeletionLogEntry[]
}
interface RetainedDataRecord {
dataType: string
reason: string
legalBasis: string
scheduledDeletionDate: Date
}
interface DeletionLogEntry {
system: string
dataType: string
deletedAt: Date
success: boolean
error?: string
}
class ErasureService {
// Systems that contain user data
private dataSystems = [
{ name: 'primary_db', handler: this.deleteFromDatabase.bind(this) },
{ name: 'search_index', handler: this.deleteFromSearchIndex.bind(this) },
{ name: 'file_storage', handler: this.deleteFromFileStorage.bind(this) },
{ name: 'email_provider', handler: this.deleteFromEmailProvider.bind(this) },
{ name: 'analytics', handler: this.deleteFromAnalytics.bind(this) },
{ name: 'cache', handler: this.deleteFromCache.bind(this) },
{ name: 'logs', handler: this.pseudonymizeInLogs.bind(this) },
]
async processErasureRequest(userId: string): Promise<ErasureRequest> {
const request: ErasureRequest = {
id: crypto.randomUUID(),
userId,
requestedAt: new Date(),
status: 'processing',
completedAt: null,
retainedData: [],
deletionLog: [],
}
// Check for data that must be retained (legal obligations)
const retentionChecks = await this.checkRetentionObligations(userId)
request.retainedData = retentionChecks
// Delete from each system
for (const system of this.dataSystems) {
try {
await system.handler(userId, retentionChecks)
request.deletionLog.push({
system: system.name,
dataType: 'all_user_data',
deletedAt: new Date(),
success: true,
})
} catch (error) {
request.deletionLog.push({
system: system.name,
dataType: 'all_user_data',
deletedAt: new Date(),
success: false,
error: (error as Error).message,
})
}
}
// Determine final status
const allSucceeded = request.deletionLog.every(entry => entry.success)
request.status = allSucceeded ? 'completed' : 'partial'
request.completedAt = new Date()
// Store the erasure request record (required for compliance)
await this.storeErasureRecord(request)
return request
}
private async deleteFromDatabase(
userId: string,
retained: RetainedDataRecord[]
): Promise<void> {
const retainedTypes = retained.map(r => r.dataType)
// Delete user profile (if not retained)
if (!retainedTypes.includes('profile')) {
await db.query('DELETE FROM user_profiles WHERE user_id = $1', [userId])
}
// Delete activity logs
await db.query('DELETE FROM activity_logs WHERE user_id = $1', [userId])
// Delete preferences
await db.query('DELETE FROM user_preferences WHERE user_id = $1', [userId])
// Pseudonymize order records (must retain for financial records)
await db.query(`
UPDATE orders SET
customer_name = 'DELETED_USER',
customer_email = 'deleted@deleted.com',
shipping_address = 'DELETED',
phone = NULL
WHERE user_id = $1
`, [userId])
// Delete the user account itself
await db.query('DELETE FROM users WHERE id = $1', [userId])
}
private async checkRetentionObligations(
userId: string
): Promise<RetainedDataRecord[]> {
const retained: RetainedDataRecord[] = []
// Check for unpaid invoices (legal obligation to retain)
const invoices = await db.query(
'SELECT COUNT(*) FROM invoices WHERE user_id = $1 AND paid = false',
[userId]
)
if (invoices.rows[0].count > 0) {
retained.push({
dataType: 'invoices',
reason: 'Outstanding financial obligations',
legalBasis: 'legal_obligation',
scheduledDeletionDate: new Date(Date.now() + 7 * 365 * 24 * 60 * 60 * 1000),
})
}
// Check for tax records (7-year retention requirement)
const taxRecords = await db.query(
'SELECT COUNT(*) FROM tax_records WHERE user_id = $1',
[userId]
)
if (taxRecords.rows[0].count > 0) {
retained.push({
dataType: 'tax_records',
reason: 'Tax compliance requirement',
legalBasis: 'legal_obligation',
scheduledDeletionDate: new Date(Date.now() + 7 * 365 * 24 * 60 * 60 * 1000),
})
}
return retained
}
}
Chapter 4: Encryption and Pseudonymization
Application-Level Encryption
Database-level encryption (transparent data encryption, TDE) protects against physical theft of storage media. Application-level encryption protects against compromised database credentials, SQL injection attacks, unauthorized database access, and insider threats from database administrators.
// field-encryption.ts
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto'
class FieldEncryptor {
private algorithm = 'aes-256-gcm'
private keyLength = 32
private ivLength = 16
private tagLength = 16
constructor(private masterKey: string) {}
encrypt(plaintext: string): string {
const iv = randomBytes(this.ivLength)
const key = scryptSync(this.masterKey, 'salt', this.keyLength)
const cipher = createCipheriv(this.algorithm, key, iv)
let encrypted = cipher.update(plaintext, 'utf8', 'hex')
encrypted += cipher.final('hex')
const tag = cipher.getAuthTag()
// Format: iv:tag:ciphertext
return iv.toString('hex') + ':' + tag.toString('hex') + ':' + encrypted
}
decrypt(encryptedData: string): string {
const parts = encryptedData.split(':')
const iv = Buffer.from(parts[0], 'hex')
const tag = Buffer.from(parts[1], 'hex')
const encrypted = parts[2]
const key = scryptSync(this.masterKey, 'salt', this.keyLength)
const decipher = createDecipheriv(this.algorithm, key, iv)
decipher.setAuthTag(tag)
let decrypted = decipher.update(encrypted, 'hex', 'utf8')
decrypted += decipher.final('utf8')
return decrypted
}
}
// Usage with database models
const encryptor = new FieldEncryptor(process.env.ENCRYPTION_KEY!)
// Encrypt before storing
const encryptedEmail = encryptor.encrypt(user.email)
await db.query(
'INSERT INTO users (id, email_encrypted, name) VALUES ($1, $2, $3)',
[user.id, encryptedEmail, user.name]
)
// Decrypt after reading
const row = await db.query('SELECT email_encrypted FROM users WHERE id = $1', [userId])
const email = encryptor.decrypt(row.rows[0].email_encrypted)
Pseudonymization
Pseudonymization replaces directly identifying data with artificial identifiers while maintaining the ability to re-link the data if needed. This is different from anonymization (which is irreversible). Pseudonymized data is still personal data under GDPR, but it benefits from reduced regulatory requirements.
// pseudonymization.ts
import { createHmac } from 'crypto'
class Pseudonymizer {
constructor(private secret: string) {}
// Generate a consistent pseudonym for a value
// Same input always produces the same pseudonym
pseudonymize(value: string, context: string): string {
const hmac = createHmac('sha256', this.secret)
hmac.update(context + ':' + value)
return 'pseudo_' + hmac.digest('hex').substring(0, 16)
}
// For analytics: pseudonymize user ID to prevent
// linking analytics data back to real users
pseudonymizeForAnalytics(userId: string): string {
return this.pseudonymize(userId, 'analytics')
}
// For logs: replace real email with pseudonym
pseudonymizeEmail(email: string): string {
const [, domain] = email.split('@')
const pseudoLocal = this.pseudonymize(email, 'email')
return pseudoLocal + '@' + domain
}
}
Chapter 5: Data Breach Response
GDPR requires notification of a personal data breach to the supervisory authority within 72 hours of becoming aware of it. If the breach is likely to result in high risk to individuals, they must also be notified without undue delay.
// breach-response.ts
interface BreachReport {
id: string
detectedAt: Date
detectedBy: string
category: 'unauthorized_access' | 'data_loss' | 'data_alteration' | 'disclosure'
affectedDataCategories: string[]
estimatedAffectedUsers: number
description: string
containmentActions: string[]
notificationRequired: boolean
supervisoryAuthorityNotified: boolean
usersNotified: boolean
timeline: BreachTimelineEntry[]
}
interface BreachTimelineEntry {
timestamp: Date
action: string
actor: string
}
class BreachResponseService {
// 72-hour countdown starts when breach is detected
private readonly NOTIFICATION_DEADLINE_HOURS = 72
async initiateBreachResponse(
description: string,
category: BreachReport['category'],
affectedData: string[]
): Promise<BreachReport> {
const report: BreachReport = {
id: crypto.randomUUID(),
detectedAt: new Date(),
detectedBy: 'automated_monitoring',
category,
affectedDataCategories: affectedData,
estimatedAffectedUsers: 0,
description,
containmentActions: [],
notificationRequired: false,
supervisoryAuthorityNotified: false,
usersNotified: false,
timeline: [{
timestamp: new Date(),
action: 'Breach detected and response initiated',
actor: 'system',
}],
}
// Step 1: Contain the breach
await this.containBreach(report)
// Step 2: Assess the impact
await this.assessImpact(report)
// Step 3: Determine if notification is required
report.notificationRequired = this.isNotificationRequired(report)
// Step 4: Set up deadline monitoring
if (report.notificationRequired) {
await this.scheduleNotificationDeadline(report)
}
// Store the report
await this.storeBreachReport(report)
// Alert the incident response team
await this.alertIncidentTeam(report)
return report
}
private isNotificationRequired(report: BreachReport): boolean {
// Notification is required if the breach is likely to result
// in a risk to the rights and freedoms of individuals
const highRiskCategories = ['financial', 'credentials', 'sensitive']
const hasHighRiskData = report.affectedDataCategories
.some(cat => highRiskCategories.includes(cat))
return hasHighRiskData || report.estimatedAffectedUsers > 100
}
}
Chapter 6: Automated Data Retention and Deletion
// data-retention.ts
class DataRetentionService {
// Run daily via cron job
async enforceRetentionPolicies(): Promise<void> {
const policies = [
{
table: 'session_logs',
dateColumn: 'created_at',
retentionDays: 30,
action: 'delete' as const,
},
{
table: 'activity_logs',
dateColumn: 'timestamp',
retentionDays: 90,
action: 'delete' as const,
},
{
table: 'user_analytics',
dateColumn: 'recorded_at',
retentionDays: 365,
action: 'anonymize' as const,
},
{
table: 'audit_logs',
dateColumn: 'created_at',
retentionDays: 2555, // 7 years
action: 'delete' as const,
},
]
for (const policy of policies) {
const cutoffDate = new Date()
cutoffDate.setDate(cutoffDate.getDate() - policy.retentionDays)
if (policy.action === 'delete') {
const result = await db.query(
`DELETE FROM ${policy.table} WHERE ${policy.dateColumn} < $1`,
[cutoffDate]
)
console.log(
`Retention: Deleted ${result.rowCount} rows from ${policy.table}`
)
} else if (policy.action === 'anonymize') {
const result = await db.query(`
UPDATE ${policy.table}
SET user_id = 'anonymous',
ip_address = '0.0.0.0',
user_agent = 'anonymized'
WHERE ${policy.dateColumn} < $1
AND user_id != 'anonymous'
`, [cutoffDate])
console.log(
`Retention: Anonymized ${result.rowCount} rows in ${policy.table}`
)
}
}
}
}
Privacy engineering is not a feature you add at the end — it's a foundational architectural decision that shapes how you design databases, APIs, logging, analytics, and deployment infrastructure. The organizations that treat privacy as a technical requirement from day one avoid costly refactoring, regulatory fines, and reputation damage.
ZeonEdge offers privacy engineering consulting, GDPR compliance audits, and privacy-by-design architecture services. We help engineering teams build systems that are compliant by design, not by afterthought. Contact our privacy engineering team for a compliance assessment of your application architecture.
Emily Watson
Technical Writer and Developer Advocate who simplifies complex technology for everyday readers.