As part of our effort to [Limit Table Sizes](../database_size_limits/_index.md), the database team is developing a data retention policy framework that
other teams will follow in order to actively limit their table sizes.
Today, retention and data lifecycle management on GitLab.com is handled inconsistently. Individual teams solve it per table,
with no shared vocabulary for describing the retention window, no agreed way of enforcing it, and no single place to
record why a given decision was made. The result is that table growth is discovered reactively, and remediation work
is designed from scratch each time.
This document proposes a unified, declarative framework for configuring the data lifecycle of a table, so that a
retention decision is made once, recorded next to the table, and enforced by tooling.
## Recommended framework
A unified, declarative framework for configuring the data lifecycle of a table. The fields below are added as
top-level keys in `db/docs/data_retention/<table_name>.yml`, and validated in CI. Because the configuration lives in
its own dedicated file, the fields do not need to be nested under a wrapping key.
Enforcement tooling will be set in place to verify that retention is actively being applied to all the tables in question.
| `exclude` | Excludes the table from data retention, with a reason | Object with `reason`: `indefinite_retention`, `technical_complexity`; unset means not excluded |
| `retention_window` | How long the data remains in the database since its initial creation | Numeric (days); `-1` only when `exclude` is set |
| `enforcement_strategy` | How the retention window is enforced in our systems | `drop_partition`, `delete_rows`, `transient_data`; `none` only when `exclude` is set |
| `enforcing` | Whether the retention policy is actively enforced | Boolean |
| `work_item` | The issue or epic recording the justification and enforcement plan | Issue or epic reference |
| `pause_mechanism` | How retention can be disabled for this table | `none`, `application_setting`, `feature_flag` |
| `pause_mechanism::name` | The name of the application setting or feature flag | String |
#### Exclude
Excludes the table from data retention. Unset by default, meaning the table is subject to retention. A table is
excluded by setting `exclude.reason` to one of a small, fixed list of values. No database team approval is required —
exclusion is self-served by picking a valid reason and recording the justification in the `work_item`. CI validates
that `reason` is one of the allowed values.
**Allowed reasons:**
1.`indefinite_retention` — data must be retained indefinitely, for example for compliance or audit purposes, or a
core entity table (for example `organizations`) that does not accumulate rows that can be aged out. This is
a stopgap: the goal is to eventually move indefinitely retained data out of the hot OLTP database into cold storage,
so tables using this reason are expected to be revisited.
1.`technical_complexity` — a table larger than the 50 GB soft limit that cannot be partitioned, so `drop_partition`
is not achievable. The specific technical constraints that make partitioning impossible MUST be justified in the
`work_item`.
Being technically difficult to delete from is **not**, on its own, a valid exclusion reason. If a table can remove
data — even if doing so is awkward or requires engineering effort — it is not excluded; it uses `delete_rows` (BBO) and
records the reasoning in the `work_item`. The exception is `technical_complexity`, which is reserved specifically for
large tables where partitioning is not possible, with the constraints justified in the `work_item`.
When `exclude.reason` is set, the two coupled fields are fixed:
1.`retention_window` MUST be `-1`.
1.`enforcement_strategy` MUST be `none`.
The coupling is bidirectional: `retention_window: -1` and `enforcement_strategy: none` are valid **only** when
`exclude.reason` is set. In every other case both fields MUST be set to concrete values following the
[decision tree](#enforcement-strategy). CI validation on these fields enforces this coupling.
#### Retention window
How long the data remains in the database since its initial creation.
**Value type:** numeric (days). `-1` is valid only when `exclude.reason` is set (see [Exclude](#exclude)); in all
other cases this field MUST be set to a positive number of days.
#### Enforcement strategy
How the retention window is enforced in our systems.
**Potential values:**
1.`drop_partition` — table is partitioned and partitions are dropped once they age past `retention_window`.
1.`delete_rows` (BBO) — data cannot be retrieved after deletion.
1.`transient_data` — data tied to a user or feature lifecycle that is already deleted as part of that lifecycle.
1.`none` — the table is excluded from retention. Valid only when `exclude.reason` is set (see [Exclude](#exclude)).
Outside the excluded case, this field MUST be set to `drop_partition`, `delete_rows`, or `transient_data` following the
decision tree.
```mermaid
flowchart TD
A{"Is the data naturally deleted<br>by the feature lifecycle?"}
A -->|Yes| B(["transient_data<br>(no-op; handled by the feature lifecycle)"])
A -->|No| G{"Can the data be deleted?"}
G -->|No| I(["none<br>(set exclude.reason: indefinite_retention)"])