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: DanceDanceVideo é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