Soft Delete
Regulated workloads often forbid hard deletes: a customer record removed today may need to be reconstructed for a chargeback in three years. Soft delete preserves the row, marks it as deleted, and scopes every subsequent read so the tombstoned row is invisible to the application.Schema attribute
@@soft_deletetakes no arguments- one model can declare it at most once
deleted_at. The model
must declare a nullable DateTime? field that maps to this column.
Runtime behaviour
For a soft-delete model:delete(id)issuesUPDATE table SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL- if the model also declares
@version, the same statement bumps the version column find_unique,find_many,update, anddeleteall adddeleted_at IS NULLto their predicatesdeleteagainst an already-tombstoned row matches zero rows and surfaces asnot found
Interaction with optimistic locking
The soft-deleteUPDATE includes version = version + 1. Callers
holding a stale ETag cannot re-tombstone a row that has already moved on,
and the post-delete version is observable to subsequent reads that the
review tooling performs directly against the table.
Interaction with audit
A soft delete records anAuditOperation::Delete event with the full
before snapshot. The audit row’s data outlives any future cold-storage
migration of the tombstoned row; the framework keeps both in step but
manages neither’s retention.
What this is not
- not a “trash bin” with restore semantics — there is no
undeletehelper; banks that need restore call SQL directly - not a substitute for backups — a
DROP TABLEremoves both live and tombstoned rows - not a cascade engine — child rows are not automatically tombstoned when a parent is. Reference-counted cleanup is application policy
When to use it
Apply@@soft_delete to:
- customer / account / counterparty records
- transfer instructions and reservations that may need to be reviewed after settlement
- anything a regulator can request the historical state of
- genuinely ephemeral data (session tokens, throttle buckets)
- tables that already have an immutable event-source upstream
- tables under a strict “right to be forgotten” obligation — hard delete is the correct behaviour there
Read Next
- Optimistic locking —
@versionpairs with@@soft_deleteso reviewers see coherent state - Audit log — the canonical “what happened” log when the row itself stops being visible