Auto-delete expired events via daily scheduled cleanup job
All checks were successful
CI / backend-test (push) Successful in 58s
CI / frontend-test (push) Successful in 23s
CI / frontend-e2e (push) Successful in 1m10s
CI / build-and-publish (push) Has been skipped

Adds a Spring @Scheduled job (daily at 03:00) that deletes all events
whose expiry_date is before CURRENT_DATE using a native SQL DELETE.
RSVPs are cascade-deleted via the existing FK constraint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 21:58:35 +01:00
parent 2a6a658df9
commit 4bfaee685c
14 changed files with 499 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
# Implementation Plan: Auto-Delete Expired Events
**Branch**: `013-auto-delete-expired` | **Date**: 2026-03-09 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/013-auto-delete-expired/spec.md`
## Summary
Add a scheduled background job that runs daily and deletes all events whose `expiryDate` has passed. Deletion is performed via a native SQL query (`DELETE FROM events WHERE expiry_date < CURRENT_DATE`). Cascade deletion of RSVPs is handled by the existing `ON DELETE CASCADE` FK constraint. No API or frontend changes required.
## Technical Context
**Language/Version**: Java 25, Spring Boot 3.5.x
**Primary Dependencies**: Spring Scheduling (`@Scheduled`), Spring Data JPA (for native query)
**Storage**: PostgreSQL (existing, Liquibase migrations)
**Testing**: JUnit 5, Spring Boot Test, Testcontainers (existing)
**Target Platform**: Linux server (Docker)
**Project Type**: Web service (backend only for this feature)
**Performance Goals**: N/A — daily batch job on small dataset
**Constraints**: Single native DELETE query, no entity loading
**Scale/Scope**: Self-hosted, small-scale — typically < 100 events total
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Privacy by Design | PASS | Deleting expired data supports privacy goals. No PII logged. |
| II. Test-Driven Methodology | PASS | Tests written before implementation (TDD). |
| III. API-First Development | N/A | No API changes — this is a backend-internal job. |
| IV. Simplicity & Quality | PASS | Single query, no over-engineering. |
| V. Dependency Discipline | PASS | Uses only existing Spring dependencies (`@Scheduled`). |
| VI. Accessibility | N/A | No frontend changes. |
## Project Structure
### Documentation (this feature)
```text
specs/013-auto-delete-expired/
├── plan.md # This file
├── spec.md # Feature specification
├── research.md # Phase 0 output (minimal — no unknowns)
├── data-model.md # Phase 1 output
└── checklists/
└── requirements.md # Spec quality checklist
```
### Source Code (repository root)
```text
backend/src/main/java/de/fete/
├── application/service/
│ └── ExpiredEventCleanupJob.java # NEW: Scheduled job
├── domain/port/out/
│ └── EventRepository.java # MODIFIED: Add deleteExpired method
└── adapter/out/persistence/
├── EventJpaRepository.java # MODIFIED: Add native DELETE query
└── EventPersistenceAdapter.java # MODIFIED: Implement deleteExpired
backend/src/test/java/de/fete/
├── application/service/
│ └── ExpiredEventCleanupJobTest.java # NEW: Unit test for job
└── adapter/out/persistence/
└── EventPersistenceAdapterIntegrationTest.java # NEW or MODIFIED: Integration test for native query
```
**Structure Decision**: Backend-only change. Follows existing hexagonal architecture: port defines the contract, adapter implements with native query, service layer schedules the job.
## Design
### Hexagonal Flow
```
@Scheduled trigger
→ ExpiredEventCleanupJob (application/service)
→ EventRepository.deleteExpired() (domain/port/out)
→ EventPersistenceAdapter.deleteExpired() (adapter/out/persistence)
→ EventJpaRepository native query (adapter/out/persistence)
→ DELETE FROM events WHERE expiry_date < CURRENT_DATE
```
### Key Decisions
1. **Port method**: `int deleteExpired()` on `EventRepository` — returns count of deleted events for logging.
2. **Native query**: `@Modifying @Query(value = "DELETE FROM events WHERE expiry_date < CURRENT_DATE", nativeQuery = true)` on `EventJpaRepository`.
3. **Schedule**: `@Scheduled(cron = "0 0 3 * * *")` — runs daily at 03:00 server time. Low-traffic window.
4. **Logging**: INFO-level log after each run: `"Deleted {count} expired event(s)"`. No log if count is 0 (or DEBUG-level).
5. **Transaction**: The native DELETE runs in a single transaction — atomic, no partial state.
6. **Enable scheduling**: Add `@EnableScheduling` to `FeteApplication` (or a config class).