The General Data Protection Regulation (GDPR) takes effect in May 2018. That seems far away, but engineering work to comply starts now.

I’ve spent the last few months working with our legal and product teams on GDPR readiness. As engineers, we’re responsible for building systems that protect user privacy and enable compliance.

GDPR isn’t just a legal checkbox. It’s a fundamental shift in how we handle personal data. The penalties are severe—up to 4% of annual revenue or €20 million, whichever is higher.

Here’s what engineers need to know about GDPR and how to build compliant systems.

What is GDPR?

GDPR is an EU regulation that governs how organizations collect, store, and process personal data of EU residents.

Who it applies to:

  • Any organization processing EU residents’ data
  • Doesn’t matter where your company is based
  • If you have EU users, GDPR applies to you

What is personal data:

  • Name, email, phone number
  • IP address, cookie IDs
  • Location data
  • Financial information
  • Health records
  • Anything that identifies or could identify a person

Most applications handle personal data. GDPR probably applies to you.

Key GDPR Principles for Engineers

1. Lawful Basis for Processing

You need a legal reason to process personal data:

  • Consent: User explicitly agrees
  • Contract: Necessary to fulfill a contract
  • Legal obligation: Required by law
  • Legitimate interest: Necessary for business operations

Most applications rely on consent or contract.

Engineering implication: Track the legal basis for each data point.

type UserData struct {
    Email       string `json:"email"`
    LegalBasis  string `json:"-"` // "consent" or "contract"
    ConsentDate time.Time `json:"-"`
}

// Store consent record
type ConsentRecord struct {
    UserID      string
    DataType    string // "email", "location", "analytics"
    Purpose     string // "marketing", "service", "analytics"
    LegalBasis  string
    ConsentedAt time.Time
    RevokedAt   *time.Time
}

2. Data Minimization

Only collect data you actually need.

Bad:

type User struct {
    Email           string
    Name            string
    Phone           string
    Address         string
    DateOfBirth     time.Time
    SocialSecurity  string
    CreditCard      string
    BrowsingHistory []string
    // ... collecting everything just in case
}

Good:

type User struct {
    Email    string // Required for login
    Name     string // Required for personalization
    // That's it. Collect more only when needed.
}

3. Storage Limitation

Don’t keep data forever. Delete when no longer needed.

type DataRetentionPolicy struct {
    DataType        string
    RetentionPeriod time.Duration
    DeletionMethod  string
}

var retentionPolicies = []DataRetentionPolicy{
    {
        DataType:        "user_account",
        RetentionPeriod: 0, // Keep while account active
        DeletionMethod:  "hard_delete",
    },
    {
        DataType:        "order_history",
        RetentionPeriod: 7 * 365 * 24 * time.Hour, // 7 years (tax requirement)
        DeletionMethod:  "anonymize",
    },
    {
        DataType:        "analytics_events",
        RetentionPeriod: 90 * 24 * time.Hour, // 90 days
        DeletionMethod:  "hard_delete",
    },
    {
        DataType:        "server_logs",
        RetentionPeriod: 30 * 24 * time.Hour, // 30 days
        DeletionMethod:  "hard_delete",
    },
}

// Automated deletion job
func cleanupExpiredData() {
    for _, policy := range retentionPolicies {
        threshold := time.Now().Add(-policy.RetentionPeriod)

        switch policy.DeletionMethod {
        case "hard_delete":
            db.Exec("DELETE FROM ? WHERE created_at < ?", policy.DataType, threshold)
        case "anonymize":
            db.Exec("UPDATE ? SET user_id = NULL, email = NULL WHERE created_at < ?",
                policy.DataType, threshold)
        }
    }
}

Run this job daily.

4. Privacy by Design

Build privacy into systems from the start, not as an afterthought.

Pseudonymization:

Don’t use real user IDs everywhere. Use pseudonyms:

// Don't log real user IDs
log.Info("User logged in", "user_id", userID) // ❌

// Use pseudonym
log.Info("User logged in", "user_hash", hashUserID(userID)) // ✓

func hashUserID(userID string) string {
    hash := sha256.Sum256([]byte(userID + secretSalt))
    return hex.EncodeToString(hash[:8]) // First 8 bytes
}

Encryption at rest:

Encrypt personal data in databases:

type EncryptedUser struct {
    ID              string
    EncryptedEmail  []byte
    EncryptedName   []byte
    EncryptionKeyID string
}

func (u *EncryptedUser) Decrypt(kms KeyManagementService) (*User, error) {
    email, err := kms.Decrypt(u.EncryptionKeyID, u.EncryptedEmail)
    if err != nil {
        return nil, err
    }

    name, err := kms.Decrypt(u.EncryptionKeyID, u.EncryptedName)
    if err != nil {
        return nil, err
    }

    return &User{
        ID:    u.ID,
        Email: string(email),
        Name:  string(name),
    }, nil
}

5. Security of Processing

Implement appropriate security measures:

  • Encryption (in transit and at rest)
  • Access controls
  • Regular security testing
  • Incident response procedures

Already covered in previous posts. GDPR makes this legally required.

Engineering Requirements

Requirement 1: Right to Access

Users can request all data you have about them.

Implementation:

type DataExport struct {
    UserID       string
    ExportedAt   time.Time
    PersonalData map[string]interface{}
}

func exportUserData(userID string) (*DataExport, error) {
    export := &DataExport{
        UserID:       userID,
        ExportedAt:   time.Now(),
        PersonalData: make(map[string]interface{}),
    }

    // User profile
    var user User
    db.First(&user, "id = ?", userID)
    export.PersonalData["profile"] = user

    // Orders
    var orders []Order
    db.Find(&orders, "user_id = ?", userID)
    export.PersonalData["orders"] = orders

    // Analytics events
    var events []AnalyticsEvent
    db.Find(&events, "user_id = ?", userID)
    export.PersonalData["analytics"] = events

    // Consent records
    var consents []ConsentRecord
    db.Find(&consents, "user_id = ?", userID)
    export.PersonalData["consents"] = consents

    return export, nil
}

// API endpoint
func handleDataExportRequest(w http.ResponseWriter, r *http.Request) {
    userID := getCurrentUser(r)

    export, err := exportUserData(userID)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("Content-Disposition", "attachment; filename=user-data.json")
    json.NewEncoder(w).Encode(export)
}

Users receive all their data in a machine-readable format.

Requirement 2: Right to Rectification

Users can correct inaccurate data.

Implementation:

Standard update APIs with audit logging:

func updateUserProfile(userID string, updates map[string]interface{}) error {
    // Update database
    if err := db.Model(&User{}).Where("id = ?", userID).Updates(updates).Error; err != nil {
        return err
    }

    // Audit log
    auditLog := AuditLog{
        UserID:    userID,
        Action:    "user_profile_updated",
        Changes:   updates,
        Timestamp: time.Now(),
    }
    db.Create(&auditLog)

    return nil
}

Requirement 3: Right to Erasure (“Right to be Forgotten”)

Users can request deletion of their data.

Implementation:

type DeletionRequest struct {
    UserID      string
    RequestedAt time.Time
    CompletedAt *time.Time
    Status      string // "pending", "processing", "completed"
}

func requestDataDeletion(userID string) error {
    request := &DeletionRequest{
        UserID:      userID,
        RequestedAt: time.Now(),
        Status:      "pending",
    }
    return db.Create(request).Error
}

// Background job processes deletion requests
func processDeletionRequests() {
    var requests []DeletionRequest
    db.Find(&requests, "status = ?", "pending")

    for _, request := range requests {
        // Update status
        db.Model(&request).Update("status", "processing")

        // Delete from all systems
        if err := deleteUserData(request.UserID); err != nil {
            log.Error("Deletion failed", "user_id", request.UserID, "error", err)
            db.Model(&request).Update("status", "failed")
            continue
        }

        // Mark complete
        now := time.Now()
        db.Model(&request).Updates(map[string]interface{}{
            "status":       "completed",
            "completed_at": &now,
        })
    }
}

func deleteUserData(userID string) error {
    // Delete from primary database
    db.Delete(&User{}, "id = ?", userID)
    db.Delete(&Order{}, "user_id = ?", userID)
    db.Delete(&AnalyticsEvent{}, "user_id = ?", userID)

    // Delete from cache
    redis.Del("user:" + userID)

    // Delete from search index
    elasticsearch.Delete("users", userID)

    // Delete from data warehouse
    warehouse.Query("DELETE FROM users WHERE user_id = ?", userID)

    // Delete from backup storage
    // (This is tricky - may need to wait for backup rotation)

    return nil
}

Challenges:

  • Backups: How do you delete from backups?

    • Solution: Document backup retention and delete when backups expire
    • Or: Encrypt backups with per-user keys and delete the key
  • Logs: User IDs in logs?

    • Solution: Use pseudonymous IDs in logs
    • Or: Set log retention to 30 days
  • Data warehouse: Replicated data?

    • Solution: Track all data locations and delete from each
  • Legal obligations: Some data must be kept (tax records)

    • Solution: Anonymize instead of delete

Requirement 4: Right to Data Portability

Users can export data in a machine-readable format for transfer to another service.

Implementation:

func exportPortableData(userID string) ([]byte, error) {
    export := make(map[string]interface{})

    // User profile (standardized format)
    var user User
    db.First(&user, "id = ?", userID)
    export["profile"] = map[string]interface{}{
        "email":      user.Email,
        "name":       user.Name,
        "created_at": user.CreatedAt,
    }

    // Orders (standardized format)
    var orders []Order
    db.Find(&orders, "user_id = ?", userID)
    export["orders"] = orders

    // Export as JSON
    return json.MarshalIndent(export, "", "  ")
}

// Alternatively, support standard formats
func exportAsCSV(userID string) ([]byte, error) {
    var buffer bytes.Buffer
    writer := csv.NewWriter(&buffer)

    // Write orders as CSV
    writer.Write([]string{"Order ID", "Date", "Amount", "Status"})
    var orders []Order
    db.Find(&orders, "user_id = ?", userID)
    for _, order := range orders {
        writer.Write([]string{
            order.ID,
            order.CreatedAt.Format(time.RFC3339),
            fmt.Sprintf("%.2f", order.Amount),
            order.Status,
        })
    }

    writer.Flush()
    return buffer.Bytes(), nil
}

Requirement 5: Right to Restrict Processing

Users can limit how their data is used.

Implementation:

type ProcessingRestriction struct {
    UserID      string
    DataType    string // "marketing", "analytics", "profiling"
    Restricted  bool
    UpdatedAt   time.Time
}

func checkProcessingAllowed(userID, dataType string) bool {
    var restriction ProcessingRestriction
    err := db.First(&restriction, "user_id = ? AND data_type = ?", userID, dataType).Error

    if err != nil {
        return true // No restriction = allowed
    }

    return !restriction.Restricted
}

// Before processing
func sendMarketingEmail(userID string) error {
    if !checkProcessingAllowed(userID, "marketing") {
        return nil // Skip silently
    }

    // Send email
    return sendEmail(userID, marketingTemplate)
}

func trackAnalyticsEvent(userID string, event Event) error {
    if !checkProcessingAllowed(userID, "analytics") {
        return nil // Skip tracking
    }

    // Track event
    return analytics.Track(userID, event)
}

Proper consent is critical for GDPR compliance.

  • Freely given: Not coerced
  • Specific: Purpose clearly stated
  • Informed: User understands what they’re agreeing to
  • Unambiguous: Clear affirmative action
  • Withdrawable: Easy to revoke

Implementation

type ConsentManager struct {
    db *gorm.DB
}

func (cm *ConsentManager) RequestConsent(userID, purpose, description string) error {
    consent := ConsentRecord{
        UserID:      userID,
        Purpose:     purpose,
        Description: description,
        Status:      "pending",
        RequestedAt: time.Now(),
    }
    return cm.db.Create(&consent).Error
}

func (cm *ConsentManager) GrantConsent(userID, purpose string) error {
    now := time.Now()
    return cm.db.Model(&ConsentRecord{}).
        Where("user_id = ? AND purpose = ?", userID, purpose).
        Updates(map[string]interface{}{
            "status":       "granted",
            "consented_at": &now,
        }).Error
}

func (cm *ConsentManager) RevokeConsent(userID, purpose string) error {
    now := time.Now()
    return cm.db.Model(&ConsentRecord{}).
        Where("user_id = ? AND purpose = ?", userID, purpose).
        Updates(map[string]interface{}{
            "status":     "revoked",
            "revoked_at": &now,
        }).Error
}

func (cm *ConsentManager) HasConsent(userID, purpose string) bool {
    var consent ConsentRecord
    err := cm.db.First(&consent,
        "user_id = ? AND purpose = ? AND status = 'granted'",
        userID, purpose).Error

    return err == nil
}

// Use in application
func processUserData(userID string) error {
    consentMgr := &ConsentManager{db: db}

    if !consentMgr.HasConsent(userID, "analytics") {
        return errors.New("no consent for analytics")
    }

    // Process data
    return trackAnalytics(userID)
}
<form action="/consent" method="post">
    <h2>Privacy Preferences</h2>

    <label>
        <input type="checkbox" name="consent_marketing" value="granted">
        <strong>Marketing Communications</strong>
        <p>We'd like to send you occasional emails about new features and promotions.
           You can unsubscribe at any time.</p>
    </label>

    <label>
        <input type="checkbox" name="consent_analytics" value="granted">
        <strong>Analytics</strong>
        <p>Help us improve our service by allowing us to analyze how you use our app.
           This data is anonymized and never sold.</p>
    </label>

    <label>
        <input type="checkbox" name="consent_personalization" value="granted">
        <strong>Personalization</strong>
        <p>Allow us to personalize your experience based on your preferences and usage patterns.</p>
    </label>

    <button type="submit">Save Preferences</button>
</form>

Granular, specific, and revokable.

Data Breach Notification

GDPR requires breach notification within 72 hours.

Detection

Monitor for anomalies:

// Detect unusual data access patterns
type AccessMonitor struct {
    redis *redis.Client
}

func (am *AccessMonitor) RecordAccess(userID, accessor string) error {
    key := fmt.Sprintf("access:%s:%s", userID, accessor)

    // Increment access count
    count, err := am.redis.Incr(key).Result()
    if err != nil {
        return err
    }

    // Set expiry (1 hour window)
    am.redis.Expire(key, 1*time.Hour)

    // Alert if unusual volume
    if count > 100 {
        alert := SecurityAlert{
            Type:     "unusual_access",
            UserID:   userID,
            Accessor: accessor,
            Count:    count,
            Message:  fmt.Sprintf("User %s accessed by %s %d times in 1 hour", userID, accessor, count),
        }
        return sendSecurityAlert(alert)
    }

    return nil
}

Notification Process

type DataBreach struct {
    ID                string
    DetectedAt        time.Time
    NotifiedAt        *time.Time
    Description       string
    AffectedUsers     []string
    DataTypes         []string
    Severity          string
    ContainedAt       *time.Time
    RootCause         string
}

func reportBreach(breach *DataBreach) error {
    // Store breach record
    db.Create(breach)

    // Notify internal security team (immediately)
    notifySecurityTeam(breach)

    // Notify affected users (within 72 hours)
    if breach.Severity == "high" {
        for _, userID := range breach.AffectedUsers {
            sendBreachNotification(userID, breach)
        }
    }

    // Notify supervisory authority (within 72 hours)
    if requiresAuthorityNotification(breach) {
        notifySupervisoryAuthority(breach)
    }

    // Update notification timestamp
    now := time.Now()
    db.Model(breach).Update("notified_at", &now)

    return nil
}

func requiresAuthorityNotification(breach *DataBreach) bool {
    // High risk to users' rights and freedoms
    return breach.Severity == "high" ||
        contains(breach.DataTypes, "financial") ||
        contains(breach.DataTypes, "health") ||
        contains(breach.DataTypes, "credentials")
}

Data Processing Agreements

If you use third-party services (AWS, analytics, payment processors), you need Data Processing Agreements (DPAs).

What to check

  • Does vendor have GDPR-compliant DPA?
  • Where is data stored (EU vs. US vs. other)?
  • What are their security practices?
  • What are their data retention policies?
  • Can they support data deletion requests?

Vendor inventory

type DataProcessor struct {
    Name            string
    Purpose         string
    DataTypes       []string
    DataLocation    string
    DPASigned       bool
    DPASignedDate   time.Time
    ComplianceLevel string // "adequate", "standard_clauses", "binding_rules"
}

var dataProcessors = []DataProcessor{
    {
        Name:            "AWS",
        Purpose:         "infrastructure",
        DataTypes:       []string{"all"},
        DataLocation:    "EU (Frankfurt)",
        DPASigned:       true,
        DPASignedDate:   time.Date(2016, 5, 1, 0, 0, 0, 0, time.UTC),
        ComplianceLevel: "adequate",
    },
    {
        Name:            "Stripe",
        Purpose:         "payment_processing",
        DataTypes:       []string{"financial", "name", "email"},
        DataLocation:    "US",
        DPASigned:       true,
        DPASignedDate:   time.Date(2016, 6, 15, 0, 0, 0, 0, time.UTC),
        ComplianceLevel: "standard_clauses",
    },
}

Maintain this inventory. Review annually.

Cross-Border Data Transfers

Transferring data outside the EU requires safeguards.

Options

  1. Adequacy decision: EU approves destination country’s privacy laws
  2. Standard contractual clauses: Use EU-approved contracts
  3. Binding corporate rules: For large multinationals
  4. Explicit consent: User agrees to transfer

Implementation

func transferData(userID string, destination string) error {
    // Check if transfer is allowed
    if !isEUCountry(destination) && !hasAdequacyDecision(destination) {
        // Need explicit consent or standard clauses
        if !hasConsentForTransfer(userID, destination) {
            return errors.New("no consent for data transfer to " + destination)
        }
    }

    // Encrypt data before transfer
    data, err := getUserData(userID)
    if err != nil {
        return err
    }

    encryptedData, err := encryptForTransfer(data)
    if err != nil {
        return err
    }

    // Log transfer
    logDataTransfer(userID, destination)

    // Perform transfer
    return sendToDestination(encryptedData, destination)
}

Practical Implementation Checklist

Phase 1: Data Inventory (Weeks 1-2)

  • Identify all personal data collected
  • Document legal basis for each data type
  • Map data flows (where data goes)
  • Inventory third-party processors
  • Sign DPAs with vendors

Phase 2: Technical Implementation (Weeks 3-8)

  • Implement data export functionality
  • Implement data deletion functionality
  • Build consent management system
  • Add encryption for personal data
  • Implement access controls
  • Set up data retention policies
  • Create audit logging

Phase 3: Processes (Weeks 9-12)

  • Document breach notification process
  • Create privacy policy
  • Train staff on GDPR requirements
  • Establish data protection officer (if required)
  • Implement privacy impact assessments
  • Set up user request workflow

Phase 4: Testing (Weeks 13-16)

  • Test data export
  • Test data deletion (including all systems)
  • Test consent withdrawal
  • Security audit
  • Penetration testing
  • Disaster recovery drill

Common Pitfalls

Pitfall 1: Assuming Anonymization is Easy

// Not actually anonymous
type AnonymizedUser struct {
    Location string  // "San Francisco"
    Age      int     // 37
    Gender   string  // "M"
    Industry string  // "Tech"
}
// Can still re-identify from combination of attributes

True anonymization is hard. Consider pseudonymization instead.

Pitfall 2: Forgetting About Logs

// PII in logs ❌
log.Info("User logged in", "email", user.Email, "ip", req.RemoteAddr)

// Better ✓
log.Info("User logged in", "user_hash", hashUserID(user.ID))

Pitfall 3: Incomplete Deletion

Don’t forget:

  • Database backups
  • Log files
  • Cache layers
  • Search indexes
  • Data warehouses
  • Third-party services

Document all data locations.

Keep records of:

  • When consent was requested
  • What was shown to user
  • When user consented
  • What they consented to
  • When consent was withdrawn

You may need to prove consent was properly obtained.

Resources and Tools

Frameworks:

Tools:

  • OneTrust (consent management)
  • TrustArc (privacy management)
  • BigID (data discovery and classification)

Open Source:

Conclusion

GDPR compliance requires engineering work:

  • Build data export and deletion
  • Implement consent management
  • Encrypt personal data
  • Set up data retention
  • Track data processors
  • Document everything

Start now. May 2018 will arrive faster than you think.

The key principles:

  • Privacy by design: Build it in from the start
  • Data minimization: Collect only what’s needed
  • Transparency: Tell users what you’re doing
  • User control: Let users manage their data
  • Security: Protect data appropriately

GDPR isn’t just compliance—it’s good engineering practice. Treating user data with respect builds trust.

In my next post, I’ll cover scalable encryption architectures for cloud applications—building systems that can encrypt massive amounts of data efficiently.

Privacy matters. Build systems that respect it.