Las condiciones de carrera en apps CRUD pueden causar pedidos duplicados y totales erróneos. Aprende puntos comunes de colisión y soluciones prácticas con constraints, locks y protecciones en la interfaz.

Una condición de carrera ocurre cuando dos (o más) peticiones actualizan los mismos datos casi al mismo tiempo, y el resultado final depende del orden y la sincronización. Cada petición parece correcta por sí sola. Juntas, producen un resultado erróneo.
Un ejemplo simple: dos personas hacen clic en Guardar sobre el mismo registro de cliente en el lapso de un segundo. Una actualiza el correo y la otra el teléfono. Si ambas solicitudes envían el registro completo, la segunda escritura puede sobrescribir la primera y un cambio desaparece sin error.
Esto ocurre más en apps rápidas porque los usuarios pueden disparar más acciones por minuto. También se dispara en momentos de alta carga: ventas flash, cierre de mes, una gran campaña de email, o cualquier momento en que muchas peticiones golpean las mismas filas.
Los usuarios rara vez reportan "una condición de carrera." Informan síntomas: pedidos o comentarios duplicados, actualizaciones perdidas ("Lo guardé y volvió atrás"), totales raros (inventario negativo, contadores que retroceden), o estados que cambian inesperadamente (aprobado y luego vuelve a pendiente).
Los reintentos lo empeoran. La gente hace doble clic, actualiza tras una respuesta lenta, envía desde dos pestañas o sufre redes inestables que hacen que navegadores o apps móviles reenvíen. Si el servidor trata cada petición como una escritura nueva, puedes obtener dos creaciones, dos cargos o dos cambios de estado que debían ocurrir una vez.
La mayoría de las apps CRUD parecen simples: leer una fila, cambiar un campo, guardarlo. El problema es que tu app no controla el tiempo. La base de datos, la red, los reintentos, trabajos en segundo plano y el comportamiento de los usuarios se solapan.
Un detonador común es que dos personas editan el mismo registro. Ambas cargan los mismos valores "actuales", ambas hacen cambios válidos y la última guardada sobrescribe silenciosamente la primera. Nadie hizo nada mal, pero una actualización se pierde.
También puede pasar con una sola persona. Un doble clic en Guardar, tocar atrás y adelante, o una conexión lenta que empuja a alguien a pulsar Enviar otra vez puede mandar la misma escritura dos veces. Si el endpoint no es idempotente, puedes crear duplicados, cobrar dos veces o avanzar un estado dos pasos.
El uso moderno añade más solapamientos. Varias pestañas o dispositivos con la misma cuenta pueden disparar actualizaciones en conflicto. Trabajos en segundo plano (emails, facturación, sincronización, limpieza) pueden tocar las mismas filas que las peticiones web. Reintentos automáticos en el cliente, balanceador o runner de trabajos pueden repetir una petición que ya tuvo éxito.
Si lanzas funcionalidades rápido, el mismo registro se actualiza desde más sitios de los que cualquiera recuerda. Si usas un creador guiado por chat como Koder.ai, la app puede crecer aún más rápido, así que vale tratar la concurrencia como comportamiento normal, no como un caso extremo.
Las condiciones de carrera rara vez aparecen en demos de "crear un registro". Aparecen donde dos peticiones tocan la misma verdad casi al mismo momento. Saber los puntos calientes habituales te ayuda a diseñar escrituras seguras desde el día uno.
Todo lo que parezca "simplemente sumar 1" puede romperse bajo carga: likes, contadores de vistas, totales, números de factura, números de ticket. El patrón arriesgado es: leer el valor, sumar y luego escribirlo de nuevo. Dos peticiones pueden leer el mismo valor inicial y sobrescribirse mutuamente.
Flujos como Draft -> Submitted -> Approved -> Paid parecen sencillos, pero las colisiones son comunes. El problema comienza cuando dos acciones son posibles a la vez (aprobar y editar, cancelar y pagar). Sin salvaguardas, puedes acabar con un registro que salta pasos, vuelve atrás o muestra distintos estados en diferentes tablas.
Trata los cambios de estado como un contrato: permite solo el siguiente paso válido y rechaza cualquier otro.
Asientos disponibles, conteos de stock, huecos de cita y campos de "capacidad restante" crean el clásico problema de sobreventa. Dos compradores hacen checkout al mismo tiempo, ambos ven disponibilidad y ambos terminan con éxito. Si la base de datos no es el juez final, acabarás vendiendo más de lo que tienes.
Algunas reglas son absolutas: un email por cuenta, una suscripción activa por usuario, un carrito abierto por usuario. Estas suelen fallar cuando primero verificas ("¿existe uno?") y luego insertas. Bajo concurrencia, ambas peticiones pueden pasar la comprobación.
Si estás generando flujos CRUD rápidamente (por ejemplo, chateando tu app en Koder.ai), apunta estos puntos calientes temprano y respáldalos con restricciones y escrituras seguras, no solo verificaciones en la interfaz.
Muchas condiciones de carrera comienzan con algo mundano: la misma acción se envía dos veces. Los usuarios hacen doble clic. La red está lenta y vuelven a pulsar. Un teléfono registra dos toques. A veces no es intencional: la página se refresca tras un POST y el navegador ofrece reenviar el formulario.
Cuando eso ocurre, tu backend puede ejecutar dos creaciones o actualizaciones en paralelo. Si ambas tienen éxito, obtienes duplicados, totales erróneos o un cambio de estado que se ejecuta dos veces (por ejemplo, aprobar y luego aprobar otra vez). Parece aleatorio porque depende del tiempo.
El enfoque más seguro es defensa en profundidad. Arregla la interfaz, pero asume que la interfaz fallará.
Cambios prácticos que puedes aplicar a la mayoría de los flujos de escritura:
Ejemplo: un usuario pulsa "Pagar factura" dos veces en móvil. La UI debería bloquear el segundo toque. El servidor también debería rechazar la segunda petición al ver la misma clave de idempotencia, devolviendo el resultado original en lugar de cobrar dos veces.
Los campos de estado parecen simples hasta que dos cosas intentan cambiarlos a la vez. Un usuario pulsa Aprobar mientras un trabajo automático marca el mismo registro como Expirado, o dos miembros del equipo trabajan el mismo ítem en pestañas distintas. Ambas actualizaciones pueden tener éxito, pero el estado final depende del orden, no de tus reglas.
Trata el estado como una pequeña máquina de estados. Mantén una tabla corta de movimientos permitidos (por ejemplo: Draft -> Submitted -> Approved, y Submitted -> Rejected). Luego cada escritura verifica: "¿Está permitida esta transición desde el estado actual?" Si no, recházala en lugar de sobrescribir silenciosamente.
El bloqueo optimista te ayuda a detectar actualizaciones obsoletas sin bloquear a otros usuarios. Añade un número de versión (o updated_at) y exige que coincida al guardar. Si alguien cambió la fila después de que la cargaste, tu actualización afecta a cero filas y puedes mostrar un mensaje claro como "Este elemento cambió, actualiza y vuelve a intentarlo."
Un patrón simple para actualizaciones de estado es:
Además, concentra los cambios de estado en un solo lugar. Si las actualizaciones están repartidas entre pantallas, jobs en segundo plano y webhooks, perderás una regla. Ponlas detrás de una única función o endpoint que haga cumplir las mismas comprobaciones cada vez.
El bug de contador más común parece inofensivo: la app lee un valor, suma 1 y lo escribe de nuevo. Bajo carga, dos peticiones pueden leer el mismo número y ambas escribir el mismo nuevo número, por lo que un incremento se pierde. Es fácil pasarlo por alto porque "normalmente funciona" en pruebas.
Si un valor solo se incrementa o decrementa, deja que la base de datos lo haga en una sola sentencia. Entonces la base de datos aplica los cambios de forma segura incluso cuando muchas peticiones llegan a la vez.
UPDATE posts
SET like_count = like_count + 1
WHERE id = $1;
La misma idea aplica a inventario, contadores de vistas, contadores de reintento y cualquier cosa que pueda expresarse como "nuevo = viejo + delta".
Los totales suelen fallar cuando guardas un número derivado (order_total, account_balance, project_hours) y luego lo actualizas desde varios lugares. Si puedes calcular el total a partir de filas fuente (líneas de pedido, asientos en libro mayor), evitas una clase entera de bugs.
Cuando debes almacenar un total por rendimiento, trátalo como una escritura crítica. Mantén las actualizaciones de filas fuente y del total almacenado en la misma transacción. Asegura que solo un escritor pueda actualizar el mismo total a la vez (bloqueos, actualizaciones condicionadas o una ruta de propietario único). Añade constraints que impidan valores imposibles (por ejemplo, inventario negativo). Luego reconcilia ocasionalmente con una comprobación en background que recalcule y marque discrepancias.
Un ejemplo concreto: dos usuarios añaden items al mismo carrito al mismo tiempo. Si cada petición lee cart_total, suma el precio y escribe, una de las adiciones puede desaparecer. Si actualizas los items del carrito y el total del carrito juntos en una transacción, el total se mantiene correcto incluso bajo clics paralelos.
Si quieres menos condiciones de carrera, empieza por la base de datos. El código de la app puede reintentar, expirar o ejecutarse dos veces. Una restricción en la base de datos es la última barrera que permanece correcta incluso cuando dos peticiones llegan a la vez.
Las restricciones únicas evitan duplicados que "nunca deberían ocurrir" pero ocurren: direcciones de email, números de pedido, IDs de factura o la regla "una suscripción activa por usuario". Cuando dos inscripciones llegan juntas, la base de datos acepta una fila y rechaza la otra.
Las claves foráneas previenen referencias rotas. Sin ellas, una petición puede borrar un registro padre mientras otra crea un hijo que apunta a nada, dejando filas huérfanas difíciles de limpiar.
Los check constraints mantienen valores dentro de un rango seguro y hacen cumplir reglas simples de estado. Por ejemplo, quantity >= 0, rating entre 1 y 5, o status limitado a un conjunto permitido.
Trata las fallas de constraints como resultados esperados, no como "errores de servidor." Captura violaciones de unicidad, foreign key y check, devuelve un mensaje claro como "Ese correo ya está en uso," y registra detalles para depurar sin filtrar información interna.
Ejemplo: dos personas hacen clic en "Crear pedido" dos veces durante lag. Con una restricción única en (user_id, cart_id) no obtendrás dos pedidos. Obtendrás un pedido y un rechazo limpio y explicable.
Algunas escrituras no son una sola sentencia. Lees una fila, verificas una regla, actualizas un estado y quizá insertas un registro de auditoría. Si dos peticiones hacen eso a la vez, ambas pueden pasar la comprobación y ambas escribir. Ese es el patrón clásico de fallo.
Encierra la escritura multinivel en una transacción para que todos los pasos tengan éxito juntos o ninguno lo haga. Más importante, la transacción te da un lugar para controlar quién puede cambiar la misma data al mismo tiempo.
Cuando solo un actor puede editar un registro a la vez, usa un bloqueo a nivel de fila. Por ejemplo: bloquea la fila del pedido, confirma que sigue en estado "pending", luego cámbiala a "approved" y escribe la entrada de auditoría. La segunda petición esperará, volverá a comprobar el estado y se detendrá.
Elige según la frecuencia de colisiones:
Mantén el tiempo de lock corto. Haz lo mínimo mientras lo sostienes: no llames a APIs externas, no hagas trabajos lentos ni bucles pesados. Si estás construyendo flujos en una herramienta como Koder.ai, deja la transacción solo para los pasos de base de datos y haz el resto después del commit.
Elige un flujo que pueda perder dinero o confianza cuando colisione. Uno común es: crear un pedido, reservar stock y luego marcar el pedido como confirmado.
Escribe los pasos exactos que tu código realiza hoy, en orden. Sé específico sobre qué se lee, qué se escribe y qué significa "éxito". Las colisiones se esconden en la brecha entre una lectura y una escritura posterior.
Una ruta de hardening que funciona en la mayoría de stacks:
Añade una prueba que demuestre la corrección. Ejecuta dos peticiones al mismo tiempo contra el mismo producto y cantidad. Asegura que exactamente un pedido queda confirmado y el otro falla de forma controlada (sin stock negativo, sin filas de reserva duplicadas).
Si generas apps rápido (incluyendo con plataformas como Koder.ai), esta checklist sigue valiendo para las pocas rutas de escritura que importan.
Una de las mayores causas es confiar en la UI. Botones deshabilitados y cheques en cliente ayudan, pero los usuarios pueden hacer doble clic, refrescar, abrir dos pestañas o reproducir una petición desde una conexión inestable. Si el servidor no es idempotente, los duplicados se filtran.
Otro bug silencioso: capturas un error de base de datos (como una violación de unicidad) pero continúas el flujo de trabajo de todos modos. Eso suele convertirse en "creación falló, pero igual enviamos el email" o "el pago falló, pero igual marcamos el pedido como pagado." Una vez ocurren efectos secundarios, es difícil revertirlos.
Transacciones largas también son una trampa. Si mantienes una transacción abierta mientras llamas a email, pagos o APIs de terceros, mantienes locks más tiempo del necesario. Eso aumenta esperas, timeouts y la probabilidad de que las peticiones se bloqueen entre sí.
Mezclar trabajos en segundo plano y acciones de usuario sin una única fuente de verdad crea estado dividido. Un job reintenta y actualiza una fila mientras un usuario la edita, y ambos creen que fueron los últimos en escribir.
Algunos "arreglos" que en realidad no lo hacen:
Si construyes con una herramienta chat-to-app como Koder.ai, las mismas reglas aplican: pide constraints en el servidor y límites transaccionales claros, no solo mejores protecciones en la UI.
Las condiciones de carrera suelen aparecer solo bajo tráfico real. Un repaso antes del lanzamiento puede atrapar los puntos de colisión más comunes sin reescribir todo.
Empieza por la base de datos. Si algo debe ser único (emails, números de factura, una suscripción activa por usuario), hazlo una restricción única real, no una regla de "comprobamos primero" en la app. Luego asegúrate de que tu código espere que la restricción falle a veces y devuelva una respuesta clara y segura.
Después, mira el estado. Cualquier cambio de estado (Draft -> Submitted -> Approved) debe validarse contra un conjunto explícito de transiciones permitidas. Si dos peticiones intentan mover el mismo registro, la segunda debería ser rechazada o convertirse en no-op, no crear un estado intermedio.
Una checklist práctica previa al lanzamiento:
Si construyes flujos en Koder.ai, toma esto como criterios de aceptación: la app generada debe fallar de forma segura bajo reintentos y concurrencia, no solo pasar el camino feliz.
Dos empleados abren la misma solicitud de compra. Ambos hacen clic en Aprobar en cuestión de segundos. Ambas peticiones llegan al servidor.
Lo que puede salir mal es confuso: la solicitud queda "aprobada" dos veces, se envían dos notificaciones y cualquier total ligado a aprobaciones (presupuesto usado, contador diario) puede subir en 2. Ambas actualizaciones son válidas por sí mismas, pero colisionan.
Aquí hay un plan de corrección que funciona bien con una base de datos estilo PostgreSQL.
Añade una regla que garantice que solo pueda existir un registro de aprobación por solicitud. Por ejemplo, guarda aprobaciones en una tabla separada y aplica una restricción UNIQUE en request_id. Ahora el segundo insert falla aunque el código tenga un bug.
Al aprobar, haz toda la transición en una sola transacción:
Si la segunda persona llega tarde, verá 0 filas actualizadas o un error de constraint único. En cualquier caso, solo un cambio gana.
Después del arreglo, la primera persona ve Aprobado y la confirmación normal. La segunda ve un mensaje amigable como: "Esta solicitud ya fue aprobada por otra persona. Actualiza para ver el estado más reciente." Sin cargas eternas, sin notificaciones duplicadas, sin fallos silenciosos.
Si generas un flujo CRUD en una plataforma como Koder.ai (backend en Go con PostgreSQL), puedes incorporar estas comprobaciones en la acción de aprobar una vez y reutilizar el patrón para otras acciones de "solo un ganador".
Las condiciones de carrera son más fáciles de arreglar cuando las tratas como una rutina repetible, no como una búsqueda de bugs ocasional. Centra tu esfuerzo en las pocas rutas de escritura que importan y haz que sean aburridamente correctas antes de pulir cualquier otra cosa.
Empieza por nombrar tus principales puntos de colisión. En muchas apps CRUD es el mismo trío: contadores (likes, inventario, balances), cambios de estado (Draft -> Submitted -> Approved) y envíos dobles (doble clic, reintentos, redes lentas).
Una rutina que funciona:
Si construyes sobre Koder.ai, Planning Mode es un buen lugar para mapear cada flujo de escritura como pasos y reglas antes de generar cambios en Go y PostgreSQL. Los snapshots y rollback también son útiles cuando despliegas nuevas restricciones o comportamiento de locks y quieres una manera rápida de volver atrás si aparece un caso límite.
Con el tiempo, esto se convierte en hábito: cada nueva función de escritura tiene una constraint, un plan de transacción y una prueba de concurrencia. Así las condiciones de carrera en apps CRUD dejan de ser sorpresas.