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
EmptyPartitionsHousekeepingJobsees 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-@Componentchange; adding another partitioned table is an add-a-PartitionManagerchange (ADR-0028) — the two extension seams stay distinct.
Evidence
odd-platform-api/.../housekeeping/job/EmptyPartitionsHousekeepingJob.java:13-14—abstract class EmptyPartitionsHousekeepingJob implements HousekeepingJobwithprivate final PartitionService partitionService;:17—doHousekeeping(connection)fetchespartitionService.getEmptyPastPartitions(connection, getTargetTable(), exclusions())(:21-22) and drops each viapartitionService.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"), iteratingList<HousekeepingJob>— the housekeeping subsystem's manager and cadence.odd-platform-api/.../partition/PostgreSQLPartitionCreationJob.java:40-41— the partition subsystem's distinct nightly@Scheduledcron +@SchedulerLock(name = "partitionCreationJob", …)(ADR-0028).odd-platform-api/.../housekeeping/job/HousekeepingJob.java:5-6— thevoid doHousekeeping(Connection)interface the cleanup jobs share (alongside the TTL row-cleanup jobs such asAlertHousekeepingJob).
See also
ADR-0028 — High-volume tables are range-partitioned by a scheduled forward-coverage job — the partition creation subsystem this one is kept separate from.
ADR-0046 — Housekeeping ships enabled by default — the on/off posture of the housekeeping subsystem described here.
Last updated