# Configurable decay thresholds

- **Pattern:** `ab-000868` (`data-quality-list-hygiene.data-decay.configurable-thresholds`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000868
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000868)

## Why it matters

Hardcoded decay thresholds (`90`, `180`, `120` as magic numbers scattered across cron jobs, cleanup handlers, and segment queries) create three problems. First, they drift: the cron job uses 180 days while the segment query uses 90, causing inconsistent behavior that is invisible until a contact falls into the gap. Second, they cannot be tuned for list behavior without a code deployment — if you discover your audience has a 60-day engagement cycle, changing the threshold requires finding and updating every hardcoded number across multiple files. Third, they make code review harder: a reviewer seeing `120` in a WHERE clause has no context for whether that number is intentional, inherited, or stale.

## Severity rationale

Low because scattered thresholds cause drift and maintenance friction rather than immediate data corruption, but threshold inconsistency silently produces divergent list-state across different parts of the system.

## Remediation

Centralize all decay thresholds in a single config module, backed by environment variables so they can be adjusted without a code deploy:

```ts
// src/lib/config/decay.ts
export const DECAY_CONFIG = {
  atRiskDays:       parseInt(process.env.DECAY_AT_RISK_DAYS       ?? '30'),
  inactiveDays:     parseInt(process.env.DECAY_INACTIVE_DAYS      ?? '90'),
  lapsedDays:       parseInt(process.env.DECAY_LAPSED_DAYS        ?? '180'),
  reverifyAfterDays: parseInt(process.env.REVERIFY_AFTER_DAYS     ?? '120'),
} as const
```

Import `DECAY_CONFIG` in every location that previously used a hardcoded number. A grep for numeric literals like `90`, `120`, `180` in the codebase should return zero results in list-hygiene logic after this change.

## Detection

- **ID:** `configurable-thresholds`
- **Severity:** `low`
- **What to look for:** Count all locations where decay threshold values (e.g., 90, 120, 180 days) appear in the codebase. Check whether they are configurable via environment variables, admin settings, or a config file — or whether they are hardcoded magic numbers scattered throughout the codebase. Hardcoded thresholds cannot be tuned without a code deployment. Report the ratio of locations using the central config.
- **Pass criteria:** Decay thresholds are defined in no more than 1 location (constants file, config object, or environment variable) and referenced consistently. Changing the threshold requires editing one place. Report the count of threshold references even on pass.
- **Fail criteria:** Threshold values (like `90`, `180`, `120`) are hardcoded as magic numbers in at least 2 places across the codebase, making them inconsistent or difficult to change.
- **Skip (N/A) when:** The system has no decay management logic whatsoever (covered by the reverification-trigger check above).
- **Detail on fail:** Example: `"Decay threshold of 180 days appears as magic number in 4 different files — cron job, cleanup handler, segment query, and report"`
- **Remediation:** Centralize thresholds in a config module:

  ```ts
  // src/lib/config/decay.ts
  export const DECAY_CONFIG = {
    atRiskDays:    parseInt(process.env.DECAY_AT_RISK_DAYS    ?? '30'),
    inactiveDays:  parseInt(process.env.DECAY_INACTIVE_DAYS   ?? '90'),
    lapsedDays:    parseInt(process.env.DECAY_LAPSED_DAYS     ?? '180'),
    reverifyAfterDays: parseInt(process.env.REVERIFY_AFTER_DAYS ?? '120'),
  } as const
  ```

  Import this object wherever threshold logic runs, rather than embedding numeric literals.

## External references

- iso-25010 maintainability

Taxons: code-quality

HTML version: https://auditbuffet.com/patterns/ab-000868
