Spring Boot cookbook · Intermediate

Implement soft delete with Hibernate

Let users 'delete' records that can be restored and excluded from normal queries.

Approach

Hibernate 6.4+ supports @SoftDelete, which turns DELETE into an update of a boolean column and filters deleted rows automatically. For a timestamp column and restore support, combine @SQLDelete with @SQLRestriction (Hibernate 6.3+). Add a partial index or include the flag in indexes, and plan for unique constraints on non-deleted rows.

Solution

@Entity
@Table(name = "projects")
@SQLDelete(sql = "UPDATE projects SET deleted_at = now() WHERE id = ? AND version = ?")
@SQLRestriction("deleted_at IS NULL")
public class Project {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
    private String name;
    private Instant deletedAt;
    @Version private long version;
}

public interface ProjectRepository extends JpaRepository<Project, Long> {
    // Native query bypasses @SQLRestriction to find deleted rows for restore
    @Modifying
    @Query(value = "UPDATE projects SET deleted_at = NULL WHERE id = :id", nativeQuery = true)
    int restore(long id);
}

// Unique name among non-deleted projects (PostgreSQL):
// CREATE UNIQUE INDEX ux_projects_name_active ON projects (tenant_id, name) WHERE deleted_at IS NULL;

Pitfalls

  • Unique constraints that block re-creating a 'deleted' name.
  • Soft-deleted personal data still subject to deletion requests (GDPR) — schedule hard deletes.
  • Joins from other entities still loading deleted rows in some mappings.

Variations

  • @SoftDelete(strategy = SoftDeleteType.TIMESTAMP) in newer Hibernate versions.
  • Archive tables instead of flags for large datasets.

Further reading: Hibernate: Soft delete

Reference: https://docs.jboss.org/hibernate/orm/6.6/userguide/html_single/Hibernate_User_Guide.html#soft-delete

More Spring Boot recipes