← CodeClarityLab Home
Browse by Category
+ added · updated 7d
← Back to glossary

Soft Delete Pattern

general PHP 5.0+ Beginner
debt(d7/e7/b7/t7)
d7 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7). The term's detection_hints list mysql-explain and pganalyze as tools, but these only surface symptoms (index bloat, slow queries) rather than proactively flagging missing deleted_at IS NULL filters or broken unique constraints. The automated field is explicitly 'no', meaning forgotten filters silently expose deleted data and require manual code review to find.

e7 Effort Remediation debt — work required to fix once spotted

Closest to 'cross-cutting refactor across the codebase' (e7). The quick_fix describes adding a column and filtering, but common_mistakes reveals the misuse propagates across every query in the codebase — forgotten WHERE deleted_at IS NULL clauses, missing partial indexes, broken unique constraints, and GDPR data that must be hard deleted all require touching many files and query sites, making remediation a cross-cutting refactor.

b7 Burden Structural debt — long-term weight of choosing wrong

Closest to 'strong gravitational pull' (b7). The applies_to covers both web and cli contexts. Every query written against any soft-delete-enabled table must carry the deleted_at IS NULL filter forever. Indexes, unique constraints, and ORM scopes must all account for the pattern. This imposes a persistent, system-wide tax on every future maintainer touching affected tables.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap' (t7). The misconception field states explicitly that 'soft deletes are always preferable to hard deletes for safety' — a canonical wrong belief. The pattern contradicts expectations from several directions: deleted records silently reappear when the filter is forgotten, unique constraints break for soft-deleted rows, and GDPR/PII data that appears 'deleted' to users is actually retained, directly contradicting the developer's mental model of deletion.

About DEBT scoring →

Also Known As

soft deletes deleted_at column logical delete

TL;DR

Marking records as deleted with a flag or timestamp rather than removing them, preserving data for audit trails and recovery.

Explanation

Soft delete adds a deleted_at timestamp (or is_deleted boolean) to records instead of removing them from the database. Queries filter out soft-deleted records by default. Benefits: audit trail, data recovery, referential integrity preservation, and compliance with data retention requirements. Trade-offs: queries must always filter deleted records (easy to forget), unique constraints need adjustment (unique email among non-deleted users), and tables grow without bound unless archived. PHP ORMs (Eloquent's SoftDeletes trait, Doctrine's SoftDeleteable extension) handle filtering automatically.

Watch Out

Soft-deleted records still appear in JOINs unless explicitly filtered — always verify that queries and reports exclude them correctly.

Common Misconception

Soft deletes are always preferable to hard deletes for safety. Soft deletes complicate every query (WHERE deleted_at IS NULL), can cause index bloat, and may inadvertently expose deleted data if a query forgets the filter. Use them when audit trails or recovery are genuine requirements.

Why It Matters

Soft delete marks records as deleted without removing them — preserving audit history, enabling undo functionality, and maintaining referential integrity for related records.

Common Mistakes

  • Forgetting to add WHERE deleted_at IS NULL to every query — deleted records silently appear.
  • Not indexing the deleted_at column — every query scans deleted rows unnecessarily.
  • Using soft deletes for everything — some data (PII, GDPR-covered data) must be hard deleted.
  • Not handling unique constraints — a soft-deleted email prevents re-registration with the same address.

Avoid When

  • Tables grow unbounded — soft-deleted rows accumulate forever, bloating indexes and slowing queries.
  • Unique constraints are on deletable columns — a soft-deleted record blocks re-creation of the same value.
  • Queries throughout the codebase forget the WHERE deleted_at IS NULL guard — data leaks are subtle and dangerous.
  • Compliance requirements mandate physical deletion (GDPR right to erasure) — soft delete is not erasure.

When To Use

  • Audit trails where you need to know what existed and when it was removed.
  • Undo functionality — users can restore accidentally deleted records.
  • Referential integrity — related records that reference the deleted item remain valid.
  • Business domains where 'deletion' is a state change (order cancelled) rather than destruction of data.

Code Examples

✗ Vulnerable
// Soft delete leaking into queries:
SELECT * FROM users WHERE id = 42;
-- Returns soft-deleted user — missing WHERE deleted_at IS NULL

-- Unique constraint issue:
INSERT INTO users (email) VALUES ('bob@example.com');
-- Error: duplicate — previous bob@example.com is soft-deleted but still in table
✓ Fixed
-- Schema: add deleted_at column
ALTER TABLE orders ADD COLUMN deleted_at TIMESTAMP NULL DEFAULT NULL;

-- PHP — Eloquent SoftDeletes trait
use Illuminate\Database\Eloquent\SoftDeletes;

class Order extends Model {
    use SoftDeletes; // adds deleted_at handling automatically
}

$order->delete();         // sets deleted_at, not a real DELETE
Order::withTrashed()->find($id);    // include soft-deleted
Order::onlyTrashed()->get();        // only soft-deleted
$order->restore();        // clears deleted_at

Added 15 Mar 2026
Edited 25 Mar 2026
Views 17
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings F 0 pings S 1 ping S 0 pings M 1 ping T 0 pings W 1 ping T 0 pings F 1 ping S 0 pings S 0 pings M 1 ping T 0 pings W 0 pings T 0 pings F 2 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 1 ping S 1 ping S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F
No pings yet today
No pings yesterday
Amazonbot 6 Unknown AI 3 Google 3 Perplexity 2 ChatGPT 1
crawler 12 crawler_json 2 pre-tracking 1
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: Medium
⚡ Quick Fix
Add a deleted_at timestamp column and filter on IS NULL in all queries — but always add a partial index on deleted_at IS NULL to maintain query performance as the deleted row count grows
📦 Applies To
PHP 5.0+ web cli laravel doctrine
🔗 Prerequisites
🔍 Detection Hints
Hard delete of rows needed for audit trail; no deleted_at filtering causing performance degradation; soft-deleted rows missing from unique constraints
Auto-detectable: ✗ No mysql-explain pganalyze
⚠ Related Problems
🤖 AI Agent
Confidence: Medium False Positives: Medium ✗ Manual fix Fix: Medium Context: File Tests: Update

✓ schema.org compliant