Skip to main content

ADR-003: LineDanceSpec s'ha de crear al backend amb Dance

CampValor
Data2026-01-07
EstatAccepted
DecisorsEquip LDP
RelacionatADR-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:

  1. Race conditions amb auth: El frontend feia crides abans que l'autenticació estigués llesta, provocant 401 o estats inconsistents.
  2. Spinner infinit: La lògica d'auto-creació tenia dependències circulars que deixaven la UI bloquejada sense sortida.
  3. Responsabilitat mal ubicada: El frontend era responsable de garantir integritat de dades, quan això és responsabilitat exclusiva del backend.
  4. Dificultat de manteniment: La lògica de creació estava duplicada (frontend + fallback) i era fràgil davant canvis d'autenticació o timing.
  5. 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​

BeneficiImpacte
Integritat garantidaNo hi ha Dance sense Spec (excepte legacy que es corregeix al primer accés)
Frontend simplificatMenys lògica, menys dependències d'estat d'autenticació, codi més mantenible
UX robustaMai spinner infinit; errors sempre visibles i recuperables amb retry
Idempotència claraMúltiples crides a createDefaultSpec o getOrCreateSpec són segures
Concurrència eliminadaUna sola transacció elimina race conditions entre creació i init
Responsabilitat claraBackend = integritat, Frontend = UX

Negatives​

DesavantatgeMitigació
Balls legacy sense specEndpoint /init idempotent resol el problema al primer accés (lazy-init)
Acoblament temporalEl spec es crea buit (DRAFT), no afecta funcionalitat ni UX
Cost transaccional lleugerNegligible: una INSERT addicional dins transacció existent

Alternatives considerades​

AlternativaPer què es va descartar
Frontend auto-crea via PUTRace conditions amb auth, spinner infinit, responsabilitat mal ubicada, loop de retry
Migració per crear specs a tots els ballsMassa invasiu per 500+ balls; millor lazy-init amb /init
Crear spec només quan s'accedeix a l'editorNo garanteix integritat, complica queries JOIN, inconsistent amb model 1:1 obligatori
Frontend crea spec en useEffect inicialDependè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​

ParentDependent 1:1Justificació
UserUserProfileTot usuari ha de tenir perfil per completar onboarding
EventEventDetailsTot event ha de tenir detalls per mostrar-se correctament
VenueVenueContactTota 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​

DataCanvi
2026-01-07Creació inicial amb contracte clar d'endpoint /init i anti-patterns documentats