In modern systems, data restoration is a real demand, whether due to user error, auditing, or support. The goal of this article is to present the concept of Soft Delete, along with its main strategies in an interactive way, so you can test and choose the best approach for your next project.
The Problem
Imagine a common scenario where a user deletes an important record by mistake. It could be an admin who clicked the wrong button, a client removing data that later becomes critical, or someone cleaning up what they thought was temporary. When there is no strategy for retaining or recovering this data, that simple mistake turns into a real crisis, where you have to stop everything, restore backups in an isolated environment, locate records manually, and reinsert whatever is possible, with the risk of losing everything created after the last database snapshot.
This scenario is a perfect example of Hard Delete in critical tables. Hard Delete is the physical removal of the record, usually done with a direct DELETE command on the table. In short, the data is truly removed from the database and disk. There is no immediate rollback, no audit trail by default, and without external backup, there is no way to recover it.
Here is an example of Hard Delete:
Hard delete example in a task table
Select a row
tasks
SQL Console
readonlyThis approach is dangerous for critical tables, but it is the ideal (and native) one for secondary data, temporary records, or quick registrations that do not impact the business.
The Solution
Soft Delete treats deletion as a logical state, not a physical one. For the user, the data is gone, but for the database, it was only marked as hidden. There are two main ways to implement this, which is what we call Soft Delete Patterns.
Logical Delete
The first Soft Delete Pattern is Logical Delete, with the simplest and most common approach. It consists of adding a column in the table itself to signal the record state.
Follow the steps below to understand a Logical Delete flow
Logical Delete example in a task table
Select a row
tasks
SQL Console
readonlyAs we can see, instead of physically removing the record, we only update the 'archived_at' column. This way, we can distinguish between active records and those that have been archived or deleted.
To implement Logical Delete, just add a new column to your table:
SQL SCRIPT
readonlyThis field can be a boolean or a timestamp. The important thing is that it clearly indicates whether the record is active or deleted.
The problem appears at scale. Millions of deleted records keep occupying space and dirtying indexes in the main table.
Here is a common challenge of Logical Delete:
Example of the bloated table problem with logical delete
Run a select to display all active tasks
tasks
SQL Console
readonly99% of the time you only care about active records, yet you're forced to carry the weight of millions of dead rows and remember to filter them in every single query.
Shadow Table
The second Soft Delete Pattern is Shadow Table, which is a more complex solution to solve main table bloat. Here, instead of marking the record, we move it to a secondary archive table.
Follow the steps below to understand the Shadow Table flow
Shadow table pattern example for data recovery
Select a row
tasks
archives
SQL Console
readonlyAs we can see, when a record is deleted from the 'tasks' table, it is automatically moved to the 'archives' table via a database trigger.
To implement Shadow Table, complexity increases. First, we must create the 'archives' table in our database:
SQL SCRIPT
readonlyNow we must create the function that will handle the archiving logic:
How to implement Shadow Table
readonlyThis function is responsible for taking the deleted record and inserting it into the 'archives' table.
Now we just need to create triggers for critical tables. This way, they are protected against Hard Delete. In the example below, the Shadow Table pattern is applied to three tables: users, establishments, and appointments.
How to create Shadow Table Triggers
readonlyNothing is free. While Logical Delete requires only an UPDATE, restoring data in a Shadow Table requires more complex INSERT INTO and SELECT queries, especially when foreign key relationships are involved.
In the example below, we have 3 examples of situations where you must recover deleted data.
Example of restoring archived data
archives
Mateus (mateus@email.com) contacted support saying he accidentally deleted the 'Downtown Barbershop' establishment and needs to recover the record.
Customer Fernanda Lima requested recovery of an account that was accidentally removed during a manual data cleanup.
Support received a request to restore an appointment canceled by mistake, while keeping the history for auditing.
SQL Console
readonlyThe trade-off is direct: recovering data here is not a simple UPDATE. It usually involves gathering context, rebuilding dependencies, and executing a sequence of queries with extra care.
The 3 SELECTs make it clear why modeling record_id and caused_by_id as JSONB is worth it. It creates room for composite key records without forcing the archives schema to change for every new case. The caused_by_table and caused_by_id columns also greatly improve cascade delete traceability, because they make the event source explicit.
Conclusion: Which one to choose?
In practice, the best deletion strategy depends heavily on the product stage and architecture maturity level. There is no single answer, there is a context-aware choice.
For POCs and early stages, Logical Delete usually makes more sense. It provides safety for data recovery with quick implementation, low friction, and without introducing project complexity too early.
For robust MVPs, pilots, and production-ready products, Shadow Table can be much more advantageous. Despite the higher implementation cost, it helps keep the main table cleaner, improves performance predictability, and better organizes the archiving strategy.
There are other soft delete patterns beyond the ones mentioned here. In this article, I brought the most well-known ones that I have used in practice, precisely to share decisions I tested in real scenarios.
Honorable mention to Alex Buchanan, creator of the blog atlas9.dev, who inspired me to build this article.
