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>
91 lines
4.2 KiB
Markdown
91 lines
4.2 KiB
Markdown
# 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).
|