ADR-003: LineDanceSpec s'ha de crear al backend amb Dance
| Camp | Valor |
|---|---|
| Data | 2026-01-07 |
| Estat | Accepted |
| Decisors | Equip LDP |
| Relacionat | ADR-002-line-dance-spec-model.md, saving-patterns.md |
Context
Dance té relació 1:1 amb LineDanceSpec. Inicialment, el frontend era responsable de crear l'spec quan detectava un 404 (spec no existent). Això va generar problemes crítics:
- Race conditions amb auth: El frontend feia crides abans que l'autenticació estigués llesta, provocant 401 o estats inconsistents.
- Spinner infinit: La lògica d'auto-creació tenia dependències circulars que deixaven la UI bloquejada sense sortida.
- Responsabilitat mal ubicada: El frontend era responsable de garantir integritat de dades, quan això és responsabilitat exclusiva del backend.
- Dificultat de manteniment: La lògica de creació estava duplicada (frontend + fallback) i era fràgil davant canvis d'autenticació o timing.
- Frontend auto-create és inherentment fràgil: Depèn d'auth timing, pot generar loops de retry, i complica la màquina d'estats de la UI.
Decisió
El backend crea LineDanceSpec automàticament dins la mateixa transacció que crea Dance.
Això garanteix que:
- La integritat és responsabilitat del backend (font de veritat)
- El frontend només gestiona UX (estats clars, retry)
- No hi ha race conditions entre creació i autenticació
Implementació Backend
1. Creació atòmica (norma per nous Dance)
// DanceController.java - create()
@PostMapping
@Transactional
public DanceSummaryDto create(@RequestBody CreateDanceRequest req) {
Dance saved = service.save(dance);
specService.createDefaultSpec(saved); // Mateixa transacció
return mapper.toSummary(saved);
}
// LineDanceSpecService.java - creació idempotent
@Transactional
public LineDanceSpec createDefaultSpec(Dance dance) {
// Idempotent: si ja existeix, el retorna
if (specRepo.existsById(dance.getId())) {
return specRepo.findById(dance.getId()).orElseThrow();
}
LineDanceSpec spec = LineDanceSpec.builder()
.dance(dance)
.status(LineDanceSpecStatus.DRAFT)
.sequenceCompleteness(LineDanceSequenceCompleteness.NONE)
.createdAt(Instant.now())
.updatedAt(Instant.now())
.build();
return specRepo.save(spec);
}
2. Endpoint per balls legacy (fallback)
Per balls creats abans d'aquesta decisió (sense spec), s'exposa un endpoint idempotent:
Path real: POST /api/dances/{danceId}/spec/init
// LineDanceSpecController.java
@RestController
@RequestMapping("/api/dances/{danceId}/spec")
public class LineDanceSpecController {
@PostMapping("/init")
@PreAuthorize("hasAnyRole('ADMIN', 'TEACHER')")
public LineDanceSpecDto initSpec(@PathVariable Long danceId) {
LineDanceSpec spec = service.getOrCreateSpec(danceId);
return mapper.toDto(spec);
}
}
// LineDanceSpecService.java - get-or-create idempotent
@Transactional
public LineDanceSpec getOrCreateSpec(Long danceId) {
return specRepo.findById(danceId).orElseGet(() -> {
Dance dance = danceRepo.findById(danceId)
.orElseThrow(() -> new NotFoundException("Dance not found: " + danceId));
return createDefaultSpec(dance);
});
}
Contracte de l'endpoint /init:
- Idempotent: Múltiples crides retornen el mateix resultat (no crea duplicats)
- Safe to retry: Pot cridar-se múltiples vegades sense efectes secundaris
- Només per legacy: No és la norma; la norma és crear amb el parent
- Requereix autenticació: Rol ADMIN o TEACHER
3. Frontend com a fallback (no com a norma)
El frontend només crida /init com a pla B quan detecta que un ball antic no té spec:
// DanceSpecEditPage.tsx - fallback per legacy
const specNotFound = specQuerySuccess && spec === null;
const initAttemptedRef = useRef<number | null>(null);
useEffect(() => {
if (specNotFound && canEdit && !initAttemptedRef.current) {
initAttemptedRef.current = numericId;
initSpecMutation.mutate(); // POST /api/dances/{id}/spec/init
}
}, [specNotFound, canEdit, numericId]);
Per què només com a fallback?
- Depèn de
authReady→ race conditions - Complica màquina d'estats → risc de spinner infinit
- Duplica responsabilitat → manteniment fràgil
Conseqüències
Positives
| Benefici | Impacte |
|---|---|
| Integritat garantida | No hi ha Dance sense Spec (excepte legacy que es corregeix al primer accés) |
| Frontend simplificat | Menys lògica, menys dependències d'estat d'autenticació, codi més mantenible |
| UX robusta | Mai spinner infinit; errors sempre visibles i recuperables amb retry |
| Idempotència clara | Múltiples crides a createDefaultSpec o getOrCreateSpec són segures |
| Concurrència eliminada | Una sola transacció elimina race conditions entre creació i init |
| Responsabilitat clara | Backend = integritat, Frontend = UX |
Negatives
| Desavantatge | Mitigació |
|---|---|
| Balls legacy sense spec | Endpoint /init idempotent resol el problema al primer accés (lazy-init) |
| Acoblament temporal | El spec es crea buit (DRAFT), no afecta funcionalitat ni UX |
| Cost transaccional lleuger | Negligible: una INSERT addicional dins transacció existent |
Alternatives considerades
| Alternativa | Per què es va descartar |
|---|---|
| Frontend auto-crea via PUT | Race conditions amb auth, spinner infinit, responsabilitat mal ubicada, loop de retry |
| Migració per crear specs a tots els balls | Massa invasiu per 500+ balls; millor lazy-init amb /init |
| Crear spec només quan s'accedeix a l'editor | No garanteix integritat, complica queries JOIN, inconsistent amb model 1:1 obligatori |
| Frontend crea spec en useEffect inicial | Dependències circulars amb auth, timing fràgil, anti-pattern de React |
Patró generalitzable
Aquest patró s'aplica a qualsevol entitat dependent 1:1 que sigui obligatòria per al funcionament del sistema:
Principi: Si l'entitat parent no té sentit sense el fill, el fill es crea amb el parent dins la mateixa transacció.
Exemples futurs aplicables
| Parent | Dependent 1:1 | Justificació |
|---|---|---|
User | UserProfile | Tot usuari ha de tenir perfil per completar onboarding |
Event | EventDetails | Tot event ha de tenir detalls per mostrar-se correctament |
Venue | VenueContact | Tota sala ha de tenir contacte per reserves |
Quan NO aplicar aquest patró
- Dependents opcionals (ex:
Dance→DanceVideoés opcional, no es crea per defecte) - Relacions 1:N (no es creen fills automàticament)
- Dependents pesats o costosos (ex: generar thumbnail, processar vídeo)
Historial
| Data | Canvi |
|---|---|
| 2026-01-07 | Creació inicial amb contracte clar d'endpoint /init i anti-patterns documentats |