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)
}
Consent Management
Proper consent is critical for GDPR compliance.
Consent Requirements
- 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)
}
Consent UI
<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
- Adequacy decision: EU approves destination country’s privacy laws
- Standard contractual clauses: Use EU-approved contracts
- Binding corporate rules: For large multinationals
- 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.
Pitfall 4: Missing Consent Audit Trail
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:
- GDPR Checklist for Developers: https://gdpr.eu/checklist/
- GDPR Developer Guide: https://ico.org.uk/for-organisations/
Tools:
- OneTrust (consent management)
- TrustArc (privacy management)
- BigID (data discovery and classification)
Open Source:
- GDPR patterns: https://github.com/privacy-patterns
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.