Back to insights
Engineeringprivacysecurityarchitecturecompliance

Privacy-by-design: practical patterns for teams

Actionable privacy patterns for software teams. Learn how to build privacy into your architecture from the start, not as an afterthought.

November 20, 2024
7 min read

Privacy regulations are expanding globally. GDPR in Europe, CCPA in California, PIPEDA in Canada—the list grows every year. But compliance is just the baseline. Good privacy practices build user trust and reduce business risk.

This guide covers practical patterns we apply to every project. They're not expensive or complex—they just require intention.

The core principle: minimize what you collect

The best way to protect data is to not have it in the first place.

Before adding any data collection, ask three questions:

  1. Why do we need this specific data?
  2. How long do we need to keep it?
  3. What's the risk if it's exposed?

If you can't answer all three clearly, don't collect it.

Example: User registration

Naive approach:

interface UserRegistration {
  email: string;
  password: string;
  firstName: string;
  lastName: string;
  dateOfBirth: Date;
  phoneNumber: string;
  address: string;
  // ... 20 more fields
}

Privacy-first approach:

interface UserRegistration {
  email: string;      // Required for account access
  password: string;   // Required for authentication
  displayName: string; // User-controlled identity
  // Everything else is optional, collected when needed
}

Collect the minimum for core functionality. Add fields only when there's a specific, documented reason.

Pattern 1: Separate PII from operational data

Personal Identifiable Information (PII) should be isolated from operational data. This limits blast radius if either is compromised.

┌─────────────────────────────────────────────────┐
│                  Application                     │
└─────────────────────────────────────────────────┘
         │                         │
         ▼                         ▼
┌─────────────────┐     ┌─────────────────────────┐
│   PII Service   │     │   Operations Database   │
│   (Encrypted)   │     │   (user_id references)  │
└─────────────────┘     └─────────────────────────┘
│                       │
│ - email               │ - orders
│ - name                │ - preferences  
│ - address             │ - activity_logs
│ - phone               │ - sessions
└───────────────────────┴─────────────────────────┘

Benefits:

  • Operations team can debug without accessing PII
  • PII access can be strictly audited
  • Different retention policies per datastore
  • Compliance with data residency requirements

Implementation example

// PII Service - separate database/service
class PIIService {
  async createUser(email: string, name: string): Promise<string> {
    const userId = generateSecureId();
    
    // Store PII separately
    await piiDatabase.users.create({
      id: userId,
      email: encrypt(email),
      name: encrypt(name),
      createdAt: new Date(),
    });
    
    // Log access
    await auditLog.record({
      action: "pii_create",
      userId,
      accessor: getCurrentUser(),
      timestamp: new Date(),
    });
    
    return userId;
  }
  
  async getUser(userId: string, accessor: string): Promise<User> {
    await auditLog.record({
      action: "pii_read",
      userId,
      accessor,
      timestamp: new Date(),
    });
    
    const user = await piiDatabase.users.findById(userId);
    return {
      id: user.id,
      email: decrypt(user.email),
      name: decrypt(user.name),
    };
  }
}
// Operations database only stores references
interface Order {
  id: string;
  userId: string;  // Reference to PII service
  items: OrderItem[];
  total: number;
  status: string;
  createdAt: Date;
}

Pattern 2: Encrypt at rest, always

Data at rest should be encrypted, but encryption quality matters.

Application-level encryption (recommended for sensitive fields):

import { createCipheriv, createDecipheriv, randomBytes } from "crypto";

class FieldEncryption {
  private algorithm = "aes-256-gcm";
  
  encrypt(plaintext: string, key: Buffer): EncryptedField {
    const iv = randomBytes(16);
    const cipher = createCipheriv(this.algorithm, key, iv);
    
    let encrypted = cipher.update(plaintext, "utf8", "hex");
    encrypted += cipher.final("hex");
    
    const authTag = cipher.getAuthTag();
    
    return {
      ciphertext: encrypted,
      iv: iv.toString("hex"),
      authTag: authTag.toString("hex"),
    };
  }
  
  decrypt(encrypted: EncryptedField, key: Buffer): string {
    const decipher = createDecipheriv(
      this.algorithm,
      key,
      Buffer.from(encrypted.iv, "hex")
    );
    decipher.setAuthTag(Buffer.from(encrypted.authTag, "hex"));
    
    let decrypted = decipher.update(encrypted.ciphertext, "hex", "utf8");
    decrypted += decipher.final("utf8");
    
    return decrypted;
  }
}

Key management: Never hardcode encryption keys. Use:

  • AWS KMS, GCP KMS, or Azure Key Vault for cloud deployments
  • HashiCorp Vault for self-managed infrastructure
  • Environment variables as a minimum (not ideal but better than hardcoding)

Pattern 3: Comprehensive audit logging

Every access to sensitive data should be logged. This isn't just for compliance—it's essential for incident response.

interface AuditLogEntry {
  timestamp: Date;
  action: "read" | "write" | "delete" | "export";
  resource: string;
  resourceId: string;
  accessor: {
    userId: string;
    role: string;
    ipAddress: string;
    userAgent: string;
  };
  context: {
    reason?: string;  // Why was this access needed?
    ticketId?: string; // Support ticket reference
  };
  outcome: "success" | "denied" | "error";
}

class AuditLogger {
  async log(entry: Omit<AuditLogEntry, "timestamp">) {
    const fullEntry: AuditLogEntry = {
      ...entry,
      timestamp: new Date(),
    };
    
    // Write to immutable log (S3, dedicated logging service)
    await this.writeToImmutableStore(fullEntry);
    
    // Alert on suspicious patterns
    await this.checkAlertConditions(fullEntry);
  }
  
  private async checkAlertConditions(entry: AuditLogEntry) {
    // Alert if bulk export
    if (entry.action === "export") {
      await this.alert("Data export detected", entry);
    }
    
    // Alert if unusual access time
    const hour = entry.timestamp.getHours();
    if (hour < 6 || hour > 22) {
      await this.alert("After-hours data access", entry);
    }
  }
}

What to log:

  • Who accessed the data (user ID, role)
  • What data was accessed (resource type, ID)
  • When (timestamp)
  • Where (IP address, user agent)
  • Why (business reason if available)
  • Outcome (success, denied, error)

What NOT to log:

  • The actual sensitive data values
  • Credentials or tokens

Pattern 4: Implement data retention policies

Data should have an expiration date. Implement automated cleanup.

interface RetentionPolicy {
  dataType: string;
  retentionDays: number;
  action: "delete" | "anonymize" | "archive";
}

const policies: RetentionPolicy[] = [
  { dataType: "session_logs", retentionDays: 30, action: "delete" },
  { dataType: "analytics_events", retentionDays: 365, action: "anonymize" },
  { dataType: "user_data", retentionDays: 730, action: "archive" },
  { dataType: "audit_logs", retentionDays: 2555, action: "archive" }, // 7 years
];

class RetentionEnforcer {
  async enforceAll() {
    for (const policy of policies) {
      await this.enforce(policy);
    }
  }
  
  private async enforce(policy: RetentionPolicy) {
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - policy.retentionDays);
    
    switch (policy.action) {
      case "delete":
        await this.deleteOlderThan(policy.dataType, cutoffDate);
        break;
      case "anonymize":
        await this.anonymizeOlderThan(policy.dataType, cutoffDate);
        break;
      case "archive":
        await this.archiveOlderThan(policy.dataType, cutoffDate);
        break;
    }
    
    await this.logRetentionAction(policy, cutoffDate);
  }
}

Run retention jobs regularly (daily or weekly) and monitor their execution.

Pattern 5: Handle data subject requests

GDPR, CCPA, and PIPEDA all grant individuals rights over their data. Build these capabilities from the start.

Right to access (data export)

class DataExporter {
  async exportUserData(userId: string): Promise<ExportPackage> {
    // Gather data from all services
    const [profile, orders, preferences, activityLogs] = await Promise.all([
      this.piiService.getUserProfile(userId),
      this.orderService.getUserOrders(userId),
      this.preferenceService.getUserPreferences(userId),
      this.activityService.getUserActivity(userId),
    ]);
    
    // Format in portable format
    return {
      exportDate: new Date().toISOString(),
      user: profile,
      orders: orders,
      preferences: preferences,
      activity: activityLogs,
      metadata: {
        format: "json",
        version: "1.0",
        retentionPolicy: "See privacy policy for details",
      },
    };
  }
}

Right to deletion (data erasure)

class DataEraser {
  async eraseUserData(userId: string): Promise<ErasureReport> {
    const report: ErasureReport = {
      userId,
      requestedAt: new Date(),
      actions: [],
    };
    
    // Delete from all services
    const deletions = [
      this.piiService.deleteUser(userId),
      this.orderService.anonymizeOrders(userId),
      this.preferenceService.deletePreferences(userId),
      this.activityService.deleteActivity(userId),
    ];
    
    const results = await Promise.allSettled(deletions);
    
    results.forEach((result, index) => {
      report.actions.push({
        service: ["pii", "orders", "preferences", "activity"][index],
        status: result.status === "fulfilled" ? "completed" : "failed",
        error: result.status === "rejected" ? result.reason.message : undefined,
      });
    });
    
    // Audit log the erasure
    await this.auditLog.record({
      action: "data_erasure",
      userId,
      report,
    });
    
    return report;
  }
}

Quick implementation checklist

For every new project:

  • [ ] Document what data you collect and why
  • [ ] Separate PII from operational data
  • [ ] Encrypt sensitive fields at application level
  • [ ] Encrypt all databases at rest
  • [ ] Implement TLS for all connections
  • [ ] Set up comprehensive audit logging
  • [ ] Define retention policies per data type
  • [ ] Build data export capability
  • [ ] Build data deletion capability
  • [ ] Review access controls quarterly
  • [ ] Run privacy impact assessment for new features

The business case for privacy

Beyond compliance, privacy-first design provides:

  1. Reduced breach impact: Less data means smaller blast radius
  2. Simpler compliance: Same patterns work across GDPR, CCPA, PIPEDA
  3. User trust: Privacy-conscious users increasingly choose privacy-respecting products
  4. Lower costs: Less data to store, process, and protect

Privacy isn't a constraint on your product—it's a competitive advantage.


Building a system that handles sensitive data? Let's discuss how we can help you get the architecture right from the start.

Ready to apply these ideas?

Let's discuss how we can help you build reliable software.