> For the complete documentation index, see [llms.txt](https://docs.opendatadiscovery.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.opendatadiscovery.org/developer-guides/architecture-decision-log/adr-0058-soft-delete-deletion-model.md).

# ADR-0058: Deletion is soft — DELETED is a status, not a row removal

## Status

**Accepted.** Reconstructed from the codebase on 2026-05-31; the decision is live in the source today.

## Context

A data entity can leave the catalog for several reasons — an owner deprecates it, a collector stops reporting it, an operator removes it. Hard-deleting the row the moment that happens would throw away the ability to show *that* it was deleted, to restore it, or to diagnose why it went; and it would force a synchronous cascade across the entity's entire dependent graph (lineage, ownership, metadata, alerts, metrics, task runs, dataset structure, …) onto the request that triggered it. The platform needed a deletion model that keeps deleted entities inspectable for a while and moves the expensive cascade off the request path.

## Decision

**Deletion is a status, not a row removal.** `DataEntityStatusDto` is a closed five-member enum — `UNASSIGNED`, `DRAFT`, `STABLE`, `DEPRECATED`, `DELETED` — and "deleting" an entity sets its status to `DELETED`; the row stays. Status is a settable resource property (`PUT /api/dataentities/{id}/statuses`, gated by `DATA_ENTITY_STATUS_UPDATE`). Two statuses carry a structural `isSwitchable` flag (`DRAFT`, `DEPRECATED`); a scheduled job uses it to auto-advance eligible entities to `DELETED`.

The read surfaces treat a soft-deleted entity **asymmetrically, on purpose**: list, search, and lineage filter `DELETED` out (`addSoftDeleteFilter`), so the working catalog stays clean — but the per-entity **detail read includes it** (`includeDeleted(true)`), so the UI can still render, diagnose, or restore a deleted entity.

The **physical purge is deferred to housekeeping**. A housekeeping job selects entities that have been in `DELETED` longer than a configured TTL (measured from the status-update timestamp) and physically removes them together with their entire dependent graph in one transaction. Until that window elapses, a soft-deleted entity is recoverable.

## Consequences

* A deleted entity remains **inspectable and restorable** during the TTL window — the detail view still resolves it — while disappearing from everyday list/search/lineage results.
* Deletion is **cheap at request time** (a single status write); the costly cascade across roughly two dozen dependent tables runs later, inside the housekeeping job's transaction, not on the user's request.
* There is a **grace period, then permanence**: once the TTL elapses and housekeeping runs, the entity and all its dependents are gone for good. Operators tune the window through the housekeeping TTL settings.
* The purge window is measured from the **status-update timestamp**, so the model leans on that timestamp being set whenever status changes; the housekeeping subsystem (ADR-0045) owns the purge schedule and can be opted out of (ADR-0046).
* The housekeeping purge is the **one place physical deletion cascades** (by entity id and ODDRN across the dependent graph); everywhere else in the platform, "deleted" means `status = DELETED`, and code that reads entities must opt in with `includeDeleted` to see them.

## Evidence

* `odd-platform-api/.../dto/DataEntityStatusDto.java:11-16` — the closed five-member enum `(id, isSwitchable)`: `UNASSIGNED(1,false)`, `DRAFT(2,true)`, `STABLE(3,false)`, `DEPRECATED(4,true)`, `DELETED(5,false)`; `DELETED` is a status value, not a row removal.
* `odd-platform-api/.../service/job/DataEntityStatusSwitchJob.java:21-30` — a 10-minute scheduled (ShedLock) job that fetches `getPojosForStatusSwitch()` and `changeStatusForDataEntities(pojos, DELETED)` — the auto-advance of switchable statuses to `DELETED`.
* `odd-platform-api/.../repository/reactive/ReactiveDataEntityRepositoryImpl.java:119` — `addSoftDeleteFilter(...)` excludes `DELETED` rows from list/datasource/namespace reads (`:160,168`); `:186,208,220` — the detail read sets `.includeDeleted(true)`, the deliberate asymmetry that surfaces soft-deleted entities to the detail view.
* `odd-platform-api/.../housekeeping/job/DataEntityHousekeepingJob.java:73-82` — selects `DATA_ENTITY` where `STATUS == DELETED` **and** `STATUS_UPDATED_AT <= now − dataEntityDeleteDays`, then `:84-129` physically deletes the entity and its dependent rows across \~25 tables, ending in `deleteFrom(DATA_ENTITY)` (`:124-126`).

## See also

* [ADR-0045 — Housekeeping is a separate subsystem from partition management](/developer-guides/architecture-decision-log/adr-0045-housekeeping-partition-separation.md) — the subsystem that runs the deferred physical purge.
* [ADR-0046 — Housekeeping ships enabled by default (opt-out)](/developer-guides/architecture-decision-log/adr-0046-housekeeping-opt-out-by-default.md) — so the purge runs unless an operator turns it off.
* [ADR-0073 — ODDRN is the universal identity for every entity](/developer-guides/architecture-decision-log/adr-0073-oddrn-universal-identity.md) — the purge cascades across the dependent graph keyed on the entity's ODDRN.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.opendatadiscovery.org/developer-guides/architecture-decision-log/adr-0058-soft-delete-deletion-model.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
