# 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).