Express.js
Transacciones desde Express
Explica cómo delimitar transacciones desde casos de uso, compartir conexión, manejar rollback y evitar mantener locks durante llamadas externas o responses.
- Última actualización
- Actualizada
- Nivel
- Profundización
Express.js
Explica cómo delimitar transacciones desde casos de uso, compartir conexión, manejar rollback y evitar mantener locks durante llamadas externas o responses.
Una transacción debe abarcar la operación de negocio que necesita atomicidad, no toda la request HTTP por defecto.
Una transacción agrupa cambios para que PostgreSQL confirme todos o ninguno. Express solo inicia el flujo; el boundary pertenece al caso de uso.
handler
↓
use case
↓ BEGIN
consultas y reglas
↓ COMMIT o ROLLBACK
↓
resultado HTTPCrear una orden puede requerir:
Si una etapa falla y las anteriores quedan confirmadas, el sistema entra en estado parcial.
Con pg, todas las consultas deben usar el mismo client:
const client = await pool.connect();
try {
await client.query('BEGIN');
const result = await createOrder(client, input);
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}pool.query dentro del bloque puede utilizar otra conexión y quedar fuera.
async function execute(input: CreateOrderInput) {
return unitOfWork.run(async (tx) => {
const products = await tx.products.findForUpdate(input.productIds);
const order = await tx.orders.create(input);
await tx.inventory.reserve(order.items);
await tx.outbox.add(orderCreated(order));
return order;
});
}El handler no necesita conocer BEGIN/COMMIT.
Mantén la transacción corta. No hagas dentro:
Las transacciones largas retienen locks, snapshots y conexiones del pool.
PostgreSQL no puede hacer rollback de un pago remoto. Estrategias:
pending y reconciliación.Nunca asumas atomicidad distribuida sin protocolo explícito.
SELECT * FROM inventory
WHERE product_id = $1
FOR UPDATE;Bloquea filas durante la transacción. Es claro, pero puede reducir concurrencia y causar deadlocks.
UPDATE inventory
SET quantity = quantity - $1,
version = version + 1
WHERE product_id = $2
AND version = $3
AND quantity >= $1;Si no actualiza filas, existe conflicto o stock insuficiente.
A menudo basta:
UPDATE inventory
SET quantity = quantity - $1
WHERE product_id = $2 AND quantity >= $1
RETURNING quantity;La condición evita overselling sin leer primero.
Read Committed es el default. Operaciones complejas pueden necesitar Repeatable Read o Serializable. Serializable puede abortar una transacción correcta por conflicto; la aplicación debe reintentar toda la unidad.
Subir aislamiento sin comprender el workload puede aumentar aborts y coste.
Dos transacciones que bloquean recursos en orden distinto pueden formar ciclo. PostgreSQL aborta una.
Mitigaciones:
Reintenta errores específicos como serialization failure o deadlock, con límite y jitter. La función completa debe ser segura de repetir.
No reintentes automáticamente validación, unique conflict o cualquier error desconocido.
La transacción puede confirmar y la response fallar. Por eso idempotencia sigue siendo necesaria. Atomicidad de DB no garantiza entrega de response.
Crear pedido con stock:
quantity >= requested y orden estable.Ordenar product IDs reduce deadlocks cuando dos pedidos comparten productos.
PostgreSQL usa savepoints, no transacciones realmente anidadas:
SAVEPOINT before_optional_step;Úsalos con criterio. Ocultar savepoints detrás de múltiples capas puede volver confuso qué se revierte.
Si cada request mantiene una transacción mientras espera un servicio externo, el pool se agota. Mide tiempo de adquisición y duración de transacciones.
pool.query dentro?Servicios externos resilientes trata fallos parciales, deadlines, retries y circuit breakers fuera de PostgreSQL.