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>
4.2 KiB
Implementation Plan: Auto-Delete Expired Events
Branch: 013-auto-delete-expired | Date: 2026-03-09 | Spec: 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)
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)
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
- Port method:
int deleteExpired()onEventRepository— returns count of deleted events for logging. - Native query:
@Modifying @Query(value = "DELETE FROM events WHERE expiry_date < CURRENT_DATE", nativeQuery = true)onEventJpaRepository. - Schedule:
@Scheduled(cron = "0 0 3 * * *")— runs daily at 03:00 server time. Low-traffic window. - Logging: INFO-level log after each run:
"Deleted {count} expired event(s)". No log if count is 0 (or DEBUG-level). - Transaction: The native DELETE runs in a single transaction — atomic, no partial state.
- Enable scheduling: Add
@EnableSchedulingtoFeteApplication(or a config class).