GDPR for Developers — What You Actually Need to Implement
The six lawful bases, what counts as personal data, data minimization, consent management, right to erasure, data export, retention policies, breach notification, and GDPR vs HIPAA vs CCPA.
GDPR Is an Engineering Problem
GDPR (General Data Protection Regulation) is often treated as a legal checkbox. In practice, it creates concrete engineering requirements: you need to delete user data on request, export it on request, log consent, and detect breaches within 72 hours. These are systems you build, not policies you write.
This article focuses on what developers need to implement — not the legal theory.
What Counts as Personal Data
Broader than most developers assume:
- Name, email, phone, address — obviously
- IP addresses — yes, even dynamic ones
- Cookie identifiers and device fingerprints — yes
- Location data
- Health data (also a special category with stricter rules)
- Employment and financial information
- Photos and video containing individuals
- Any identifier that can be linked to a person directly or indirectly
The test is not "does this obviously identify someone?" but "could this, combined with other data, identify someone?" When in doubt, treat it as personal data.
The Six Lawful Bases for Processing
You need a lawful basis for every type of personal data processing. The six are:
| Basis | Use When | |-------|---------| | Consent | User has freely given, specific, informed, unambiguous consent | | Contract | Processing necessary to perform a contract with the user | | Legal obligation | You're legally required to process it | | Vital interests | Life-or-death situations | | Public task | Public authority carrying out official functions | | Legitimate interests | Necessary for your legitimate business interests, balanced against user rights |
For most commercial software: contract (to run the service) and legitimate interests (analytics, fraud prevention) cover most processing. Consent is required for marketing, tracking cookies, and anything not strictly necessary.
Don't use consent as a catch-all — it must be freely revocable, and if users withdraw consent, you must stop processing and delete related data.
Data Minimization
Collect only what you need for a specific purpose. This is a design constraint, not a policy.
Before adding a field to a form or a column to a table, ask: "Is this necessary for the stated purpose?" If no, don't collect it.
// Profile creation — only collect what's needed for the service
public record CreateUserRequest
{
[Required]
public string Email { get; init; } = "";
// DON'T collect date of birth unless you need age verification
// DON'T collect phone number unless you need 2FA or delivery
// DON'T collect gender unless the service requires it
// DON'T collect address at registration — collect at checkout
}Consent Management
Consent must be:
- Freely given — no pre-ticked boxes, no bundled consent ("agree to all")
- Specific — separate consent for each purpose
- Informed — user understands what they're consenting to
- Unambiguous — a clear affirmative action (not silence or inactivity)
- Revocable — as easy to withdraw as to give
public class ConsentRecord
{
public int Id { get; set; }
public string UserId { get; set; } = "";
public string Purpose { get; set; } = ""; // "marketing_email", "analytics"
public bool Granted { get; set; }
public DateTime Timestamp { get; set; }
public string IpAddress { get; set; } = ""; // Proof of when/where consent given
public string ConsentVersion { get; set; } = ""; // Which version of privacy policy
}
public class ConsentService
{
public async Task RecordConsentAsync(
string userId,
string purpose,
bool granted,
string ipAddress,
string consentVersion)
{
await _db.ConsentRecords.AddAsync(new ConsentRecord
{
UserId = userId,
Purpose = purpose,
Granted = granted,
Timestamp = DateTime.UtcNow,
IpAddress = ipAddress,
ConsentVersion = consentVersion
});
await _db.SaveChangesAsync();
}
public async Task<bool> HasConsentAsync(string userId, string purpose)
{
// Get the most recent consent record for this purpose
var latest = await _db.ConsentRecords
.Where(c => c.UserId == userId && c.Purpose == purpose)
.OrderByDescending(c => c.Timestamp)
.FirstOrDefaultAsync();
return latest?.Granted == true;
}
}Right to Erasure (Right to Be Forgotten)
Users can request deletion of their personal data. "Deletion" is nuanced:
Hard delete: physically remove the data. Clean and definitive. Problems: breaks foreign keys, audit logs, financial records that have legal retention requirements.
Soft delete / anonymization: replace personal data with null or a placeholder. Preserve the record structure for referential integrity and auditing, but strip PII.
public async Task EraseUserDataAsync(string userId)
{
using var transaction = await _db.Database.BeginTransactionAsync();
// Anonymize the user record — don't delete, preserve audit trail
var user = await _db.Users.FindAsync(userId);
if (user != null)
{
user.Email = $"deleted_{userId}@erased.invalid";
user.Name = "Deleted User";
user.Phone = null;
user.Address = null;
user.DateOfBirth = null;
user.IsDeleted = true;
user.DeletedAt = DateTime.UtcNow;
}
// Anonymize activity logs but keep them for fraud detection
await _db.ActivityLogs
.Where(l => l.UserId == userId)
.ExecuteUpdateAsync(s => s
.SetProperty(l => l.IpAddress, "0.0.0.0")
.SetProperty(l => l.UserAgent, null));
// Hard delete marketing data — no retention need
await _db.MarketingProfiles
.Where(p => p.UserId == userId)
.ExecuteDeleteAsync();
// Hard delete sessions
await _db.UserSessions
.Where(s => s.UserId == userId)
.ExecuteDeleteAsync();
await _db.SaveChangesAsync();
await transaction.CommitAsync();
// Schedule removal from search indices, CDN caches, backups
await _erasureQueue.EnqueueAsync(new ErasureJob(userId));
}Note: some data has legal retention requirements (financial records: typically 7 years). You can retain it but must restrict access.
Right of Access (Data Export Endpoint)
Users can request a copy of all personal data you hold about them. This must be machine-readable (JSON or CSV is standard).
[Authorize]
[HttpGet("me/data-export")]
public async Task<IActionResult> ExportMyData()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)!.Value;
var export = new UserDataExport
{
ExportedAt = DateTime.UtcNow,
Profile = await _db.Users
.Where(u => u.Id == userId)
.Select(u => new { u.Email, u.Name, u.CreatedAt })
.FirstOrDefaultAsync(),
Orders = await _db.Orders
.Where(o => o.UserId == userId)
.Select(o => new { o.Id, o.Total, o.CreatedAt, o.Status })
.ToListAsync(),
ConsentHistory = await _db.ConsentRecords
.Where(c => c.UserId == userId)
.Select(c => new { c.Purpose, c.Granted, c.Timestamp })
.ToListAsync(),
ActivityLog = await _db.ActivityLogs
.Where(l => l.UserId == userId)
.OrderByDescending(l => l.Timestamp)
.Take(1000)
.Select(l => new { l.Action, l.Timestamp })
.ToListAsync()
};
return Ok(export);
}Log when export requests are made — you need to demonstrate compliance.
Data Retention Policies
Don't keep data forever. Define retention periods per data type and automate purging.
// Background service — runs nightly
public class DataRetentionJob : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await ApplyRetentionPoliciesAsync();
await Task.Delay(TimeSpan.FromHours(24), ct);
}
}
private async Task ApplyRetentionPoliciesAsync()
{
// Activity logs: 90 days
var activityCutoff = DateTime.UtcNow.AddDays(-90);
await _db.ActivityLogs
.Where(l => l.Timestamp < activityCutoff && !l.IsRetainedForLegal)
.ExecuteDeleteAsync();
// Abandoned cart data: 30 days
var cartCutoff = DateTime.UtcNow.AddDays(-30);
await _db.AbandonedCarts
.Where(c => c.UpdatedAt < cartCutoff)
.ExecuteDeleteAsync();
// Marketing consent: delete if no marketing relationship for 2 years
// (specific to your legal basis — consult legal team)
}
}Data Breach Notification
Under GDPR, you must notify the supervisory authority within 72 hours of becoming aware of a breach that's likely to result in risk to individuals. Notify affected individuals if the risk is high.
Your engineering obligation:
- Detect breaches (SIEM, anomaly detection, audit logs)
- Have an incident response runbook
- Know which data is stored where and who it affects
- Be able to determine breach scope quickly
// Audit logging — essential for breach investigation
public class AuditLog
{
public long Id { get; set; }
public string UserId { get; set; } = "";
public string Action { get; set; } = ""; // "patient.record.viewed"
public string ResourceType { get; set; } = "";
public string ResourceId { get; set; } = "";
public DateTime Timestamp { get; set; }
public string IpAddress { get; set; } = "";
public bool WasSuspicious { get; set; }
}Database-Level Protection
// Encrypt sensitive columns at rest
public class User
{
public string Id { get; set; } = "";
public string Email { get; set; } = ""; // Store encrypted in DB
// EF Core value converter for transparent encryption
// (use SQL Server Always Encrypted or application-layer encryption)
}
// EF Core — configure encrypted column
modelBuilder.Entity<User>()
.Property(u => u.Email)
.HasConversion(
v => _encryptionService.Encrypt(v),
v => _encryptionService.Decrypt(v)
);GDPR vs HIPAA vs CCPA
| | GDPR (EU) | HIPAA (US Healthcare) | CCPA (California) | |--|-----------|----------------------|-------------------| | Scope | Any EU resident's data | Protected health information | California residents | | Consent | Required for non-essential | Not the primary mechanism | Opt-out (not opt-in) | | Breach notification | 72 hours to authority | 60 days | Expedient, 30-day cure | | Right to delete | Yes | Limited (treatment records retained) | Yes | | Right to access | Yes | Yes (within 30 days) | Yes | | Fines | Up to 4% global revenue | Up to $1.9M/year | Up to $7,500/violation |
If you operate in healthcare and have EU users, you're subject to both GDPR and HIPAA — the stricter rule applies to each requirement.
Privacy by Design Checklist
- [ ] Collect only what's necessary (data minimization)
- [ ] Encrypt PII at rest and in transit
- [ ] Implement soft-delete with PII anonymization
- [ ] Build data export endpoint before launch
- [ ] Log consent with timestamp, version, and IP
- [ ] Define and automate retention periods
- [ ] Audit log all access to sensitive data
- [ ] Test the erasure and export flows
Enjoyed this article?
Explore the Security & Compliance learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.