Frontend nextjs

prisma.$transaction: por qué tu orden y tu stock son atómicos

Crear una orden y descontar stock parecen dos pasos separados, hasta que uno falla y el otro no. Así se evita ese estado inconsistente con Prisma.

7 min
Prisma Next.js Bases de datos Backend

Un checkout de e-commerce, simplificado, hace tres cosas: crea la orden, descuenta el stock de cada producto comprado, y vacía el carrito. Son tres operaciones de base de datos separadas. La pregunta que casi nadie se hace hasta que ya es tarde: ¿qué pasa si la segunda falla después de que la primera ya se ejecutó?

Te quedas con una orden creada, cobrada (o a punto de cobrarse), pero con el stock del inventario sin descontar — dos usuarios distintos podrían terminar comprando la última unidad del mismo producto. Es exactamente el tipo de bug que no aparece en desarrollo (donde todo funciona “en el camino feliz”) y sí aparece en producción, bajo carga real.

La solución: atomicidad, no reintentos

La respuesta no es “agregar manejo de errores y reintentar” — es garantizar que las tres operaciones se traten como una sola unidad indivisible: o se aplican todas, o no se aplica ninguna.

const order = await prisma.$transaction(async (tx) => {
  // 1. Crear la orden con sus items
  const newOrder = await tx.order.create({
    data: {
      fullName: 'Juan Pérez',
      email: 'juan@example.com',
      total: 24999,
      items: {
        create: cartItems.map((item) => ({
          productId: item.productId,
          quantity: item.quantity,
          price: item.product.price,
        })),
      },
    },
  })

  // 2. Descontar stock de cada producto
  for (const item of cartItems) {
    await tx.product.update({
      where: { id: item.productId },
      data: { stock: { decrement: item.quantity } },
    })
  }

  // 3. Vaciar el carrito
  await tx.cartItem.deleteMany({ where: { cartId } })

  return newOrder
})

Nota el parámetro tx — dentro del callback, todas las operaciones deben usar tx (no prisma directo). Eso es lo que las agrupa en la misma transacción. Si cualquier await dentro de ese bloque lanza un error (una restricción de base de datos, una conexión perdida, cualquier cosa), Prisma revierte automáticamente todo lo que se haya ejecutado hasta ese punto — la orden no queda creada a medias, el stock no queda descontado sin la orden correspondiente.

Por qué esto no es “optimización prematura”

Es tentador pensar que este tipo de casos son raros — “en la práctica nunca va a fallar a la mitad”. Pero las razones por las que una operación falla a mitad de camino no dependen de que tu lógica esté mal escrita: una conexión de red que se corta, un timeout de base de datos bajo carga alta, un deploy que reinicia el proceso justo en el peor momento. Ninguna de esas es controlable desde tu código de aplicación — lo único controlable es cómo reacciona tu base de datos cuando eso pasa, y ahí es donde una transacción marca la diferencia entre un estado inconsistente silencioso y una operación que simplemente no se aplicó (y puede reintentarse limpiamente).

Transacción interactiva vs. transacción por lote

Prisma soporta dos formas de usar $transaction. La de arriba es la interactiva (con un callback async (tx) => {...}), útil cuando el resultado de una operación determina la siguiente (como cuando necesitas el id de la orden recién creada para crear sus items, aunque en este caso Prisma lo resuelve con create anidado).

La otra forma, por lote, sirve cuando las operaciones son independientes entre sí:

// Transacción por lote: útil cuando las operaciones NO dependen unas de otras
await prisma.$transaction([
  prisma.product.update({ where: { id: 'a' }, data: { stock: { decrement: 1 } } }),
  prisma.product.update({ where: { id: 'b' }, data: { stock: { decrement: 2 } } }),
])

La regla para elegir: si necesitas el resultado de una operación para construir la siguiente (como el id de una orden recién creada), usa la interactiva. Si son operaciones independientes que solo necesitan ejecutarse todas-o-ninguna, el modo por lote es más simple y, en algunos motores de base de datos, algo más eficiente.

Un error común: mezclar tx y prisma en la misma transacción

// ❌ Esto rompe la atomicidad, aunque compile sin error
await prisma.$transaction(async (tx) => {
  await tx.order.create({ /* ... */ })
  await prisma.product.update({ /* ... */ }) // usó `prisma`, no `tx` — quedó FUERA de la transacción
})

Este es un bug silencioso particularmente peligroso: el código compila, funciona en pruebas manuales (porque casi nunca falla nada a la mitad durante desarrollo), y solo se manifiesta bajo la condición exacta que la transacción existía para prevenir. Vale la pena tener disciplina de revisar que cada llamada dentro del callback use tx.

Cuándo NO necesitas una transacción

No todo requiere este nivel de garantía. Si estás haciendo una sola operación de escritura (un solo update, un solo create), ya es atómica por definición — no hay “a medias” posible en una sola sentencia. Las transacciones importan específicamente cuando varias operaciones dependen de que todas se apliquen juntas para que el estado de tu sistema siga siendo válido.

Conclusión

La atomicidad no es un detalle de bases de datos avanzado reservado para sistemas bancarios — es relevante en cualquier flujo donde dos o más escrituras deben mantenerse consistentes entre sí, como una orden y su descuento de stock. prisma.$transaction hace que garantizar eso sea casi tan simple como escribir el código secuencial que ya ibas a escribir de todas formas.

¿Tu aplicación tiene flujos de negocio con múltiples escrituras que deberían ser atómicas? Agenda una revisión técnica gratuita de 15 minutos y lo evaluamos juntos.

¿Tienes un proyecto en mente?

Convierte tu idea en un producto real

Desarrollo web, aplicaciones a medida y consultoría tecnológica para empresas y startups. Cuéntame tu proyecto y te respondo en menos de 24 horas.

Solicitar presupuesto Ver LinkedIn