ADR-0045: Housekeeping is a separate subsystem from partition management

ODD Platform separates TTL row-cleanup (housekeeping) from partition creation; the one bridging task, dropping empty partitions, lives in housekeeping but calls the partition service.

Status

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

Context

Two kinds of background maintenance touch the same high-volume tables: structural lifecycle (creating range partitions ahead of need, per ADR-0028) and content cleanup (deleting aged rows by TTL, and dropping partitions once they are empty). These have different criticality and cadence — a missing partition is an insert failure that must be created reliably ahead of time; deleting old rows is best-effort cleanup that can wait. The platform had to decide whether to fold them into one mechanism or keep them apart.

Decision

The two concerns are separate subsystems, and the one task that bridges them lives on the cleanup side but delegates to the structural side. The housekeeping package runs content cleanup — a manager iterating List<HousekeepingJob> on a frequent fixed-rate schedule (every 15 minutes, ShedLock-elected under the lock name housekeepingJob). The partition package runs structural creation — the nightly forward-coverage job of ADR-0028 (ShedLock-elected under the distinct lock name partitionCreationJob). They are different packages, different schedules, and different ShedLock locks; both use ShedLock for cluster election (neither uses a Postgres advisory lock for scheduling).

Their single intersection is empty-partition dropping, and it is modelled to name both concerns explicitly at the type level: EmptyPartitionsHousekeepingJob is an abstract class that lives in the housekeeping package and implements the housekeeping HousekeepingJob interface, but consumes the PartitionService and does its work by fetching the empty past partitions (partitionService.getEmptyPastPartitions(...)) and dropping each one (partitionService.dropPartition(...)). Its concrete subclasses (ActivityEmptyPartitionsHousekeepingJob, and the message-table equivalent) register as housekeeping jobs. So dropping empty partitions runs on the housekeeping cadence and through the housekeeping manager, while the actual partition operation stays owned by the partition service — the coupling is one explicit class, not a blurring of the two subsystems.

Consequences

  • Partition creation (deployment-critical) and partition empty-drop (best-effort) run on independent schedules and locks, so the frequent housekeeping cycle never interferes with, or depends on, the once-nightly creation cycle.

  • The intersection is discoverable: a contributor reading EmptyPartitionsHousekeepingJob sees both the housekeeping interface and the partition service in one type signature, rather than partition logic hidden inside a housekeeping flow or vice-versa.

  • A contributor who wants to "consolidate all scheduled DB maintenance into one job" is working against this separation — the split (frequent content cleanup vs nightly structural creation) is the deliberate shape, and the empty-drop job is the sanctioned way to touch partitions from the cleanup side.

  • Adding another cleanup task is an add-a-HousekeepingJob-@Component change; adding another partitioned table is an add-a-PartitionManager change (ADR-0028) — the two extension seams stay distinct.

Evidence

  • odd-platform-api/.../housekeeping/job/EmptyPartitionsHousekeepingJob.java:13-14abstract class EmptyPartitionsHousekeepingJob implements HousekeepingJob with private final PartitionService partitionService; :17doHousekeeping(connection) fetches partitionService.getEmptyPastPartitions(connection, getTargetTable(), exclusions()) (:21-22) and drops each via partitionService.dropPartition(connection, partition) (:26) — the explicit cross-package coupling.

  • odd-platform-api/.../housekeeping/job/ActivityEmptyPartitionsHousekeepingJob.java:8-9@Component public class … extends EmptyPartitionsHousekeepingJob (registers as a housekeeping job).

  • odd-platform-api/.../housekeeping/HousekeepingJobManager.java:17-26@Component @ConditionalOnProperty("housekeeping.enabled", havingValue="true"), @Scheduled(fixedRate = 15, timeUnit = MINUTES) + @SchedulerLock(name = "housekeepingJob", lockAtLeastFor = "14m", lockAtMostFor = "14m"), iterating List<HousekeepingJob> — the housekeeping subsystem's manager and cadence.

  • odd-platform-api/.../partition/PostgreSQLPartitionCreationJob.java:40-41 — the partition subsystem's distinct nightly @Scheduled cron + @SchedulerLock(name = "partitionCreationJob", …) (ADR-0028).

  • odd-platform-api/.../housekeeping/job/HousekeepingJob.java:5-6 — the void doHousekeeping(Connection) interface the cleanup jobs share (alongside the TTL row-cleanup jobs such as AlertHousekeepingJob).

See also

Last updated