Conflictos y UX: los límites del optimismo
El optimismo del caso feliz es fácil; su madurez se mide en los bordes. Esta lección cierra el nivel con las tres situaciones donde el optimismo se juega su honestidad: los conflictos, cuando dos cambios chocan sobre el mismo recurso y el servidor responde con un rechazo de concurrencia; la experiencia, cómo señalar el estado en vuelo con indicadores discretos de guardando y guardado sin caer ni en el silencio que engaña ni en el spinner que anula la ventaja; y el criterio, cuándo NO ser optimista en absoluto: operaciones irreversibles, resultados impredecibles en el cliente y endpoints con alta tasa de fallo. La tesis final del nivel: el optimismo no es un ajuste que se activa por defecto, es un contrato de tres cláusulas —feedback instantáneo, señales honestas del vuelo y recuperación elegante del error— y romper cualquiera convierte la mentira útil en una traición.
El optimismo del caso feliz es la parte fácil: predices, aciertas, reconcilias, y nadie nota la costura. Su verdadera madurez se revela en los bordes, donde la apuesta se tuerce. ¿Qué ocurre si mientras tu predicción viaja, otro cliente cambió el mismo dato y el servidor te rechaza por conflicto? ¿Cómo le dices al usuario que su cambio aún no está confirmado sin arruinar la sensación de inmediatez que perseguías? ¿Y cuándo, sencillamente, no deberías ser optimista en absoluto? Esta lección cierra el nivel con las tres cuestiones que separan un optimismo de juguete de uno de producción: los conflictos entre cambios, la experiencia honesta del estado en vuelo, y el criterio para reconocer las operaciones donde adelantarse es un error. El optimismo, veremos, no es un interruptor que se enciende por defecto, sino un contrato que hay que honrar entero o no firmar.
- Manejar los conflictos de concurrencia cuando dos cambios chocan sobre el mismo recurso.
- Diseñar la experiencia del estado en vuelo con indicadores de guardando y guardado sin traicionar la inmediatez.
- Comunicar el fallo y el rollback con claridad, ofreciendo reintento en lugar de revertir en silencio.
- Aplicar el criterio para decidir cuándo NO ser optimista: lo irreversible, lo impredecible y lo que falla mucho.
Cuando dos cambios chocan
El optimismo asume implícitamente que eres el único que toca el dato, pero en un sistema real varios clientes, o varias pestañas del mismo usuario, mutan el mismo recurso a la vez. Tu predicción parte del estado que tú viste, mientras el servidor ya avanzó a otro. Cuando tu petición llega, el servidor puede detectar que la editaste sobre una versión obsoleta y rechazarla con un conflicto de concurrencia —el clásico código 409— en lugar de aplicarla a ciegas. Los mecanismos habituales son un número de versión o un identificador de revisión, a menudo transportado como ETag, que el servidor compara: si no coincide, tu escritura llega tarde.
// Concurrencia optimista: el cliente declara la version que creyo editar
const res = await fetch(url, {
method: "PUT",
headers: { "If-Match": etag }, // la revision base sobre la que edite
body: JSON.stringify(cambio),
});
if (res.status === 409) {
const fresco = await api.leer(url); // el mundo cambio: trae lo nuevo
resolverConflicto(cambio, fresco); // fusionar, preguntar o descartar
}
Fíjate en que esta es la misma idea de concurrencia optimista que abría el nivel, ahora del lado del servidor: cada quien procede como si nadie más tocara el dato y el conflicto se valida al final, cuando la escritura aterriza.
Es un error tratar el 409 como un error de red más y limitarte a revertir. Un conflicto significa que tu predicción era razonable pero el mundo cambió debajo: revertir sin más devuelve al usuario a su estado, pero le oculta que ahora existe una versión más nueva que la suya. Lo correcto suele ser reconciliar trayendo la versión fresca del servidor y, si el cambio del usuario aún tiene sentido sobre ella, ofrecerle reaplicarlo. Un conflicto no pide un rollback ciego, pide una decisión informada.
Las estrategias de resolución forman un abanico: desde el simple “gana la última escritura”, que descarta silenciosamente el cambio perdedor y es aceptable para datos triviales, hasta la fusión de cambios o la intervención explícita del usuario cuando el dato es valioso. Elegir mal aquí produce el peor bug del optimismo: la pérdida silenciosa de trabajo ajeno.
Ordenadas de menor a mayor sofisticación: gana la última escritura descarta el cambio perdedor sin avisar y sirve para datos triviales; la fusión combina cambios que no se solapan y capitula cuando sí; la resolución manual delega en el usuario la decisión ante un choque real; y en el extremo, las CRDT y la transformación operacional —la maquinaria que sostiene los editores colaborativos— hacen que las escrituras concurrentes converjan por construcción, de modo que no queda conflicto que resolver. Cada peldaño cuesta más complejidad; se sube solo hasta donde el valor del dato lo justifica.
La experiencia del estado en vuelo
Entre la predicción y la confirmación existe una ventana en la que el resultado se muestra pero no está garantizado, y la interfaz debe ser honesta sobre esa incertidumbre sin sabotear la inmediatez que era su razón de ser. El equilibrio es fino: si no señalas nada, engañas al usuario haciéndole creer que algo confirmado cuando aún no lo está; si plantas un spinner sobre cada acción, anulas la ventaja de haberte adelantado y vuelves al pesimismo con pasos extra.
Guardando
Una señal discreta y no bloqueante: opacidad reducida, un icono tenue, un texto pequeño. El resultado ya se ve y es usable; la señal solo susurra que la confirmación viaja.
Guardado
La confirmación llega y la señal se retira: el elemento pasa a su apariencia plena. El usuario aprende, sin leer nada, que su cambio quedó firme.
Fallo
El rollback ocurre y se acompaña de un aviso claro con reintento. Nunca una reversión muda: el usuario debe entender qué pasó y qué puede hacer.
flowchart LR A[accion del usuario] --> B[render optimista con senal guardando] B --> OK[confirmacion: retirar senal, estado guardado] B --> ERR[fallo: rollback con aviso y reintento] style OK fill:#a6e3a1,color:#11111b style ERR fill:#f38ba8,color:#11111b
El caso del fallo merece el mayor cuidado. Una reversión silenciosa es la peor experiencia posible: el usuario vio su acción surtir efecto y luego desaparecer sin explicación, y no sabe si debe reintentar, si perdió datos o si la aplicación está rota. Comunicar el fallo con un mensaje claro y un reintento a mano transforma un momento de desconfianza en uno de control.
Modelar esos tres estados como un tipo mantiene la interfaz honesta, porque obliga a decidir qué se muestra en cada uno en lugar de olvidar el caso incómodo:
type EnVuelo = "guardando" | "guardado" | "fallido";
function Fila({ texto, estado, onReintentar }) {
return (
<li style={{ opacity: estado === "guardando" ? 0.5 : 1 }}>
{texto}
{estado === "guardando" && <small> guardando...</small>}
{estado === "fallido" && <button onClick={onReintentar}>reintentar</button>}
</li>
);
}
Los dos fracasos de la señalización son simétricos. Señalar de menos —no marcar nada— convierte el optimismo en engaño: el usuario cree firme lo que aún es una apuesta. Señalar de más —un spinner por cada pulsación— reintroduce la ansiedad que viniste a eliminar y delata que ni tú confías en tu predicción. El punto justo es una señal periférica: visible si la buscas, ignorable si no. La confirmación, tan callada que no interrumpa; el fallo, tan claro que no pase inadvertido.
Cuándo NO ser optimista
El cierre del nivel es un ejercicio de criterio, porque la técnica más avanzada incluye saber cuándo no usarla. Tres condiciones desaconsejan el optimismo. La primera es la irreversibilidad y el alto coste: un pago, un borrado con efectos en cascada, cualquier acción donde equivocarse sea caro o embarazoso; ahí la ansiedad de un spinner honesto vale más que la falsa calma de un optimismo que habrá que desmentir. La segunda es la impredecibilidad: si el cliente no puede calcular el resultado sin el servidor, no hay predicción que mostrar. La tercera es la alta tasa de fallo: sobre una red inestable o un endpoint frágil, revertir constantemente es más desconcertante que esperar de entrada.
Irreversible o caro
Pagos, borrados en cascada, envíos definitivos. Si equivocarse cuesta dinero o vergüenza, la espera honesta vale más que la inmediatez.
Impredecible
Si el resultado depende de datos que solo el servidor posee, no hay predicción posible: no inventes lo que no puedes calcular.
Falla a menudo
Sobre una red o un endpoint frágiles, revertir a cada paso desconcierta más que un spinner franco desde el principio.
Basta con que se cumpla una de las tres para que el optimismo deje de ser un regalo y pase a ser un riesgo. El árbol de decisión lo condensa: solo el camino que sortea las tres trampas llega a la casilla donde el contrato se puede firmar.
flowchart TD Q1[el resultado es predecible en el cliente?] -->|no| PES[se pesimista, espera al servidor] Q1 -->|si| Q2[el fallo es reversible y barato?] Q2 -->|no| PES Q2 -->|si| Q3[la tasa de exito es alta?] Q3 -->|no| PES Q3 -->|si| OPT[se optimista con contrato completo] style OPT fill:#a6e3a1,color:#11111b style PES fill:#89dceb,color:#11111b
Al cerrar el nivel conviene nombrar lo que realmente aprendiste, porque es más grande que una técnica. Una actualización optimista es una promesa que le haces al usuario: tu acción casi con certeza tendrá éxito, así que voy a comportarme como si ya lo hubiera tenido. Esa promesa tiene tres cláusulas indivisibles, y honrarla significa cumplir las tres. La primera es el feedback instantáneo: el resultado aparece a la velocidad del pensamiento, que es toda la razón de ser del optimismo. La segunda es la honestidad sobre el vuelo: mientras la confirmación viaja, la interfaz no finge certeza que no tiene, sino que señala con discreción que el resultado es provisional. La tercera es la recuperación elegante: cuando la apuesta se pierde, el sistema revierte sin dejar rastro, explica lo ocurrido y ofrece un camino de vuelta. Rompe la primera y no tenías optimismo, tenías un spinner disfrazado. Rompe la segunda y tu inmediatez se vuelve un engaño, porque enseñaste como firme lo que era una apuesta. Rompe la tercera y tu apuesta perdida se convierte en trabajo perdido en silencio, la peor traición que una interfaz puede cometer contra quien confió en ella. Por eso el error de principiante no es implementar mal el optimismo, sino tratarlo como un ajuste de rendimiento que se activa por reflejo en cada mutación, cuando es en realidad una decisión de diseño con obligaciones. Todo lo que este nivel construyó —la predicción calculable, la reconciliación que se rinde ante el servidor, el snapshot que hace posible el rollback, las APIs que codifican una filosofía del tiempo— existe para poder cumplir esas tres cláusulas de forma fiable. El ingeniero maduro no pregunta cómo hacer que todo sea optimista, sino dónde el contrato se puede firmar con honestidad y dónde es más respetuoso decir la verdad incómoda: espera un momento, todavía no lo sé.
- Provoca un conflicto real editando el mismo recurso desde dos pestañas y comprueba qué hace tu servidor: ¿rechaza con
409o aplica a ciegas la última escritura? - Convierte una reversión muda en una con aviso claro y reintento, y compara la sensación de ambas ante un fallo forzado.
- Diseña las tres señales de estado —guardando, guardado y fallo— para una mutación, buscando el punto donde son honestas sin anular la inmediatez.
- Recorre las mutaciones de tu aplicación con el árbol de decisión y marca cuáles deberían dejar de ser optimistas por irreversibles, impredecibles o propensas al fallo.
- Elige una mutación optimista tuya y audita si cumple las tres cláusulas del contrato; si falla alguna, decide si la arreglas o la conviertes en pesimista honesta.
- Añade a una mutación de escritura una cabecera
If-Matchcon la versión base y maneja el409: trae la versión fresca y decide si fusionas, preguntas o descartas.