“Just Delete the User”: Famous Last Words Before the GDPR Audit
“We just got a GDPR erasure request,” your product manager says casually on Monday morning. “Should be quick, right? Just delete the user?”
Three weeks later, you’ve discovered that user’s data lives in seventeen different places. Production database, analytics warehouse, blob storage, Redis cache, Elasticsearch indexes, backup tapes, third-party CRM, email provider archives, and that legacy system nobody wants to touch. Deleting the account breaks referential integrity in six tables, crashing the order pipeline.
Welcome to ISO/IEC 27701 Control 7.3.4 and GDPR Article 17. They don’t ask you to delete data. They require orchestrated erasure across distributed systems, audit trails, third-party notifications, preserved referential integrity, and proof it all worked. All while keeping your application running.
This is where most privacy implementations die.
The Fatal Pattern: Database Scripts and Hope
Here’s what I’ve seen in production far too often:
// FATAL: The "just delete it" approach
public async Task DeleteUserAsync(Guid userId)
{
var user = await _context.Users.FindAsync(userId);
if (user != null)
{
_context.Users.Remove(user);
await _context.SaveChangesAsync();
}
// Data still in: Redis, blob storage, Elasticsearch, analytics DB,
// Mailchimp, Salesforce, 7-year backups, JSON logs with PII...
// No audit trail. No verification. Hope it worked.
}
Why this fails spectacularly:
- Referential Integrity Violations: Foreign key constraints crash your app
- Incomplete Deletion: Data persists in caches, logs, backups for years
- No Third-Party Notification: GDPR Article 19 requires it. This doesn’t.
- No Audit Trail: Can’t prove to regulators what happened
- Backup Problem: Restoring backups resurrects “forgotten” users
One company I consulted for discovered a 40% failure rate on erasure requests. Half the “deleted” users were still fully present in their analytics warehouse. The regulator audit was… educational.
Understanding the Requirements
Let’s cut through the legal jargon:
ISO 27701 Control 7.3.4 requires mechanisms for data subjects to withdraw consent, request erasure, get it done “within reasonable timeframes,” and receive confirmation.
GDPR Article 17 goes further: erasure “without undue delay,” mandatory processor notification (Article 19), documented exceptions for legal obligations. And here’s the kicker: you must demonstrate compliance to supervisory authorities.
ISO 27701 Control 7.4.8 (Disposal) adds secure disposal methods, verification that disposal is complete and irreversible, plus documentation.
Translation: orchestrated, verifiable, audited deletion across every system that touches personal data. Not a database DELETE statement.
The Correct Pattern: Orchestrated Erasure
Here’s what actually works. Using Azure Durable Functions for orchestration, soft-delete for referential integrity, and distributed coordination:
1. Erasure Request Model
public class ErasureRequest
{
public Guid RequestId { get; set; }
public Guid UserId { get; set; }
public string Email { get; set; }
public DateTime RequestedAt { get; set; }
public ErasureStatus Status { get; set; }
public List<ErasureTask> Tasks { get; set; } = [];
public DateTime? CompletedAt { get; set; }
public string? FailureReason { get; set; }
}
public enum ErasureStatus { Pending, InProgress, Completed, PartiallyCompleted, Failed }
public class ErasureTask
{
public string SystemName { get; set; } // "PrimaryDatabase", "BlobStorage", "Mailchimp"
public string TaskType { get; set; } // "Delete", "Anonymize", "Notify"
public TaskStatus Status { get; set; }
public DateTime? CompletedAt { get; set; }
public string? Details { get; set; }
}
public enum TaskStatus { Pending, InProgress, Completed, Failed, Skipped }
2. Durable Function Orchestration
The orchestrator coordinates all erasure activities. It runs parallel where possible and sequential where rate limits demand:
[FunctionName(nameof(ErasureOrchestrator))]
public async Task<ErasureResult> ErasureOrchestrator(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var request = context.GetInput<ErasureRequest>();
var result = new ErasureResult { RequestId = request.RequestId };
// Phase 1: Validate (legal holds, active contracts?)
var validation = await context.CallActivityAsync<ValidationResult>(
nameof(ValidateErasureRequest), request);
if (!validation.IsValid) return result with { Status = ErasureStatus.Failed };
// Phase 2: Parallel erasure across systems
var tasks = await Task.WhenAll(
context.CallActivityAsync<ErasureTask>(nameof(AnonymizeUserInDatabase), request),
context.CallActivityAsync<ErasureTask>(nameof(DeleteBlobStorageData), request),
context.CallActivityAsync<ErasureTask>(nameof(InvalidateCaches), request),
context.CallActivityAsync<ErasureTask>(nameof(RemoveFromSearchIndexes), request));
result.Tasks.AddRange(tasks);
// Phase 3: Sequential third-party notifications (rate limits)
foreach (var processor in await context.CallActivityAsync<List<string>>(
nameof(GetThirdPartyProcessors), request.UserId))
{
result.Tasks.Add(await context.CallActivityAsync<ErasureTask>(
nameof(NotifyThirdPartyProcessor), (request, processor)));
}
// Phase 4: Verify and audit
var verification = await context.CallActivityAsync<VerificationResult>(
nameof(VerifyErasureCompleteness), request);
result.Status = verification.IsComplete ? ErasureStatus.Completed : ErasureStatus.PartiallyCompleted;
await context.CallActivityAsync(nameof(CreateAuditRecord), result);
return result;
}
The key insight: parallel execution where systems are independent, sequential where they’re not. Third-party APIs have rate limits. Respect them or get blocked.
3. Database Anonymization with Soft Delete
Hard deletes break referential integrity. Soft delete with anonymization keeps your foreign keys happy while removing all personal data:
[FunctionName(nameof(AnonymizeUserInDatabase))]
public async Task<ErasureTask> AnonymizeUserInDatabase(
[ActivityTrigger] ErasureRequest request, ILogger log)
{
using var context = new ApplicationDbContext();
var user = await context.Users
.Include(u => u.Orders).Include(u => u.Comments)
.FirstOrDefaultAsync(u => u.Id == request.UserId);
if (user == null) return new ErasureTask { Status = TaskStatus.Skipped };
// Anonymize—keeps FK relationships intact
user.Email = $"deleted-{request.UserId}@privacy.local";
user.FirstName = user.LastName = "REDACTED";
user.PhoneNumber = user.Address = user.DateOfBirth = user.TaxId = null;
user.IsDeleted = true;
user.DeletedAt = DateTime.UtcNow;
// Related entities too
foreach (var order in user.Orders)
order.ShippingAddress = order.BillingAddress = "REDACTED";
foreach (var comment in user.Comments)
(comment.AuthorName, comment.Content) = ("Anonymous", "[Removed]");
await context.SaveChangesAsync();
return new ErasureTask { SystemName = "Database", Status = TaskStatus.Completed };
}
The email pattern (deleted-{userId}@privacy.local) is intentional. It’s unique, clearly anonymized, and lets you verify erasure later.
4. Third-Party Notification
GDPR Article 19 is non-negotiable: if you shared data with processors, you must tell them to delete it too.
[FunctionName(nameof(NotifyThirdPartyProcessor))]
public async Task<ErasureTask> NotifyThirdPartyProcessor(
[ActivityTrigger] (ErasureRequest Request, string Processor) input)
{
var response = await _httpClient.PostAsJsonAsync(
GetProcessorConfig(input.Processor).ErasureEndpoint,
new { input.Request.UserId, input.Request.Email, Action = "ERASURE_REQUIRED" });
return new ErasureTask
{
SystemName = input.Processor,
Status = response.IsSuccessStatusCode ? TaskStatus.Completed : TaskStatus.Failed
};
}
Track acknowledgments. You’re liable even if your processor fails to delete. That’s the fun part of being a data controller.
5. Verification and Audit
“Trust but verify” doesn’t cut it. Verify, then trust nothing:
[FunctionName(nameof(VerifyErasureCompleteness))]
public async Task<VerificationResult> VerifyErasureCompleteness(
[ActivityTrigger] ErasureRequest request)
{
var issues = new List<string>();
var user = await _context.Users.FindAsync(request.UserId);
if (user is { IsDeleted: false }) issues.Add("User not marked deleted");
if (user != null && !user.Email.StartsWith("deleted-")) issues.Add("Email not anonymized");
if (await _blobContainer.GetBlobsAsync(prefix: $"{request.UserId}/").AnyAsync())
issues.Add("Blobs remain");
if (await _cache.GetAsync($"user:{request.UserId}") != null)
issues.Add("Still in cache");
return new VerificationResult { IsComplete = issues.Count == 0 };
}
The audit log is your evidence when regulators ask “prove you deleted this person’s data.” Make it immutable. Cosmos DB with append-only access works well.
Testing Your Erasure Implementation
Compliance without tests is compliance on paper only:
[TestMethod]
public async Task ErasureOrchestrator_RemovesDataFromAllSystems()
{
// Arrange: seed user with data across all systems
var userId = Guid.NewGuid();
await SeedTestUser(userId, withOrders: 5, withBlobs: 3);
// Act
var result = await RunOrchestration(new ErasureRequest { UserId = userId });
// Assert: nothing remains
Assert.AreEqual(ErasureStatus.Completed, result.Status);
Assert.IsTrue((await GetUser(userId)).IsDeleted);
Assert.IsTrue((await GetUser(userId)).Email.StartsWith("deleted-"));
Assert.AreEqual(0, await GetBlobCount(userId));
Assert.IsNull(await GetCachedUser(userId));
Assert.IsNotNull(await GetAuditRecord(result.RequestId));
}
[TestMethod]
public async Task ErasureOrchestrator_PreservesReferentialIntegrity()
{
var userId = Guid.NewGuid();
await SeedTestUser(userId, withOrders: 5, withBlobs: 0);
var result = await RunOrchestration(new ErasureRequest { UserId = userId });
// Orders exist but anonymized
var orders = await GetOrders(userId);
Assert.AreEqual(5, orders.Count);
Assert.IsTrue(orders.All(o => o.ShippingAddress == "REDACTED"));
}
Test the edge cases: partial failures, third-party timeouts, concurrent erasure requests. Your orchestrator will encounter all of them.
The Backup Trap
GDPR doesn’t require deleting data from backups immediately, but restored data must be re-erased. Build a guard:
public async Task<RestoreValidationResult> ValidateRestoration(
DateTime backupDate, IEnumerable<Guid> userIdsInBackup)
{
var erasedAfterBackup = (await _auditLog.GetErasureRequestsAfter(backupDate))
.Select(r => r.UserId).ToHashSet();
var zombieUsers = userIdsInBackup.Where(erasedAfterBackup.Contains).ToList();
return new RestoreValidationResult
{
RequiresPostRestoreErasure = zombieUsers.Any(),
UsersToReErase = zombieUsers,
Message = zombieUsers.Any()
? $"Restoration will resurrect {zombieUsers.Count} deleted users. Re-erasure required."
: "Clean restore"
};
}
Every backup restoration must check the erasure audit log. Automate this or watch “forgotten” users come back to haunt you.
When Deletion Is Illegal
Not all data can be erased. Tax records, legal claims, active contracts: they override erasure requests:
public ValidationResult ValidateErasureRequest(Guid userId)
{
var blocks = new List<string>();
if (HasActiveContract(userId)) blocks.Add("Active contract requires retention");
if (HasTaxObligations(userId)) blocks.Add("Tax law: 7 years from last transaction");
if (HasPendingLegalClaims(userId)) blocks.Add("Pending legal claims");
return new ValidationResult
{
IsValid = blocks.Count == 0,
Reason = string.Join("; ", blocks)
};
}
Document your exceptions. Regulators accept legitimate retention reasons. They don’t accept “we forgot to check.”
What Actually Works
After implementing erasure workflows across multiple organizations:
Architecture: Use orchestration (Durable Functions, Step Functions, Temporal). Soft-delete with anonymization. Design for eventual consistency because some systems lag.
Data: Anonymize where you can’t delete. Immutable audit trails in separate datastores. Version your workflows because regulations evolve.
Third Parties: Document every processor’s erasure API. Test notifications regularly (APIs change). Track acknowledgments because you’re liable for their failures.
Verification: Automate checks in the orchestration. Run periodic sweeps for escaped data. Test backup restoration with erasure validation.
Operations: Monitor erasure SLAs (“without undue delay” is legally binding). Alert on failures. Practice disaster recovery with privacy in mind.
The right to erasure isn’t optional. It’s a fundamental privacy right with substantial fines behind it. Organizations that build orchestration, verification, and audit trails from day one sleep well when regulators come knocking.
The rest scramble to prove they deleted data they can’t actually prove they deleted.
Don’t be the rest.

Comments