Architecture Decision Record
ADR-005: Soft-Delete vs. Right-to-Erasure
Status
Accepted. Updated 2026-06-27 (documented the [Pii] → IAnonymizable build-time guard and PiiRedactor).
Context
The framework's default deletion model is soft-delete: AuditableBaseEntity.Delete() sets IsDeleted = true and EF Core global query filters exclude the row from normal queries. The row — including any personal data it holds — stays in the database indefinitely, which is exactly what audit, referential integrity, and "undelete" (BR-135) require.
This conflicts with data-subject erasure rights (GDPR Art. 17 right-to-be-forgotten, CCPA deletion): when a person requests deletion, their personal data must actually be removed or anonymized, not merely hidden. Soft-delete alone is therefore non-compliant for personal data, and a consumer app's published privacy policy (e.g. a "we delete your data within 30 days" promise) cannot be honored by soft-delete.
A second source of retained personal data is the outbox: processed OutboxMessage rows hold serialized event payloads that may contain personal data and were previously never purged (ADR-003).
Decision
Separate the two concerns and provide an extension point for each, rather than overloading soft-delete:
- Soft-delete stays the default for lifecycle/state management (hide + retain + undelete). It is explicitly not a privacy mechanism.
- Erasure is an explicit, additive capability. Aggregates that store personal data implement
IAnonymizable(MMCA.Common.Domain.Interfaces). An application-layer erasure handler loads the aggregate, callsAnonymize()(idempotent, returnsResult), and saves — overwriting personal fields in place so foreign keys and the audit trail survive. Fields that must remain retrievable are persisted through the AES-256-GCMEncryptedStringConverter. - Outbox retention is bounded.
OutboxCleanupServicepurges processed outbox rows older thanOutbox:RetentionDays(default 7; set0to disable) across every relational data source, so event payloads are not retained indefinitely. - A build-time guard backs the per-entity opt-in. Domain properties holding data-subject personal data are marked
[Pii](MMCA.Common.Domain.Attributes.PiiAttribute, property-targeted). A fitness function (ArchitectureRules.EntitiesWithPiiImplementAnonymizable, surfaced throughPiiConventionTestsBase) fails the build if any Domain entity declaring a[Pii]property does not implementIAnonymizable, so "this aggregate holds personal data but has no erasure path" is caught by tooling, not by review. The companionPiiRedactor(MMCA.Common.Domain.Privacy) masks the same[Pii]-marked members when an entity is written to a log or telemetry attribute, so personal data does not leak through diagnostics.
The framework provides the extension points (IAnonymizable, OutboxCleanupService, PiiRedactor) and a build-time guard (the [Pii] → IAnonymizable fitness rule); each consumer app owns the policy: which properties are [Pii], the erasure orchestration/endpoint (data-subject request handling), and any data-subject access/export endpoint — because the personal-data model lives in the consumer (e.g. ADC's User).
Rationale
- Right tool per concern: soft-delete answers "is this record active?"; erasure answers "has this person's data been removed?". Conflating them (e.g. hard-deleting inside
Delete()) would break audit, undelete, and referential integrity. - Audit-preserving: anonymize-in-place keeps the row and its audit fields, satisfying both erasure and accountability obligations simultaneously.
- Idempotent + Result-based: matches the framework's domain conventions and tolerates retried erasure requests.
- Bounded retention: the cleanup service closes the "outbox grows forever / retains PII forever" gap noted in ADR-003 without changing delivery semantics.
Trade-offs
- Erasure is opt-in per entity, but the opt-in is guarded: an aggregate exposing a
[Pii]-marked property that does not implementIAnonymizablefails the architecture fitness test, so the "holds personal data but is not erasable" gap surfaces at build time rather than in a manual personal-data inventory. The residual risk shifts to marking: an entity that holds personal data on an unmarked property is invisible to the guard, so consumers still own the discipline of applying[Pii]to genuine data-subject fields. - Anonymization is irreversible by design and is not the same operation as undelete.
- The framework cannot, on its own, make a consumer compliant: the consumer must still wire the erasure handler, the data-subject request flow, and access/export. This ADR provides the extension points, not the policy.
- The default 7-day outbox retention is a behavior change: consumers upgrading the framework begin purging processed outbox rows older than 7 days unless they set
Outbox:RetentionDays = 0.