Inventario sin vender de más
30 minIntermedio

Dos compras a la vez nunca venden la misma unidad si el stock y el pedido cambian en una sola transacción.
Cuando varios clientes compran el último producto a la vez, cada compra tiene que descontar el stock y crear su pedido sin pisar a las demás. Con una transacción multidocumento las dos escrituras ocurren juntas o no ocurre ninguna, tanto si el stock y los pedidos están en el mismo rango como si están en rangos de nodos distintos.
Lo que vas a construir
Una función placeOrder de Node.js que descuenta unidades solo si quedan y crea el pedido en la misma transacción.
Cinco compras a la vez de un producto con tres unidades dejan tres pedidos y el stock a cero.
Requisitos
- Node.js 18 o posterior.
- Docker y
mongosh, para arrancar TeselaDB en tu máquina y mirar los datos.
Prepara el entorno
docker run -d --name teseladb -p 27017:27017 viuzi/teseladb:latest \
start --dbpath /data --insecure --insecure-allow-remote --bind_ip 0.0.0.0
export TESELADB_URI="mongodb://127.0.0.1:27017/shop"
mongosh "$TESELADB_URI" --quiet --eval 'db.runCommand({ ping: 1 }).ok'
mkdir inventario && cd inventario && npm init -y && npm install mongodbEl contenedor arranca un nodo de TeselaDB sin TLS ni usuarios, que basta para seguir la guía en tu máquina. Cuando mongosh responde 1, el nodo ya acepta conexiones. TESELADB_URI apunta a ese nodo y a la base shop; en un clúster de producción la cadena lleva TLS y usuario, como cuenta Conectar tu aplicación. La última línea crea la carpeta del programa con el paquete mongodb instalado.
1. Descuenta con una condición
El filtro available: { $gte: quantity } hace que la actualización solo cambie el documento si quedan unidades
suficientes. Si no cambia nada, la función lanza un error y la transacción se deshace entera.
2. Escribe la compra en una transacción
withTransaction reintenta la transacción cuando choca con otra que tocó el mismo documento, así que no tienes que
escribir los reintentos. Guarda el programa como inventario.mjs:
import { MongoClient } from "mongodb";
const client = new MongoClient(process.env.TESELADB_URI);
const db = client.db("shop");
// Takes the units from the stock and writes the order in one transaction: both happen, or neither.
async function placeOrder(sku, quantity, customer) {
const session = client.startSession();
try {
return await session.withTransaction(async () => {
const taken = await db
.collection("stock")
.updateOne({ _id: sku, available: { $gte: quantity } }, { $inc: { available: -quantity } }, { session });
if (taken.modifiedCount === 0) throw new Error(`not enough stock for ${sku}`);
await db.collection("orders").insertOne({ sku, quantity, customer, createdAt: new Date() }, { session });
});
} finally {
await session.endSession();
}
}
await db.collection("stock").insertOne({ _id: "chair-red", available: 3 });
const customers = ["Ana", "Luis", "Marta", "Pablo", "Sara"];
const results = await Promise.allSettled(customers.map((customer) => placeOrder("chair-red", 1, customer)));
const placed = results.filter((result) => result.status === "fulfilled").length;
const { available } = await db.collection("stock").findOne({ _id: "chair-red" });
console.log(`${placed} orders placed, ${available} left`);
await client.close();Probarlo
Ejecuta node inventario.mjs contra una base vacía. Las cinco compras van a la vez, tres encuentran stock y dos fallan
con not enough stock:
3 orders placed, 0 leftLimpiar
db.getSiblingDB("shop").stock.deleteOne({ _id: "chair-red" });
db.getSiblingDB("shop").orders.deleteMany({ sku: "chair-red" });Desde mongosh, la primera orden borra el producto de prueba y la segunda borra sus pedidos. Al final borra también el contenedor con la última orden: el nodo guarda los datos dentro, sin volumen, así que con él desaparece todo lo que escribiste durante la guía y el puerto 27017 queda libre.
docker rm -f teseladbSiguientes pasos
- Transacciones, con lo que garantiza el aislamiento por instantánea.
- Reintentos y errores transitorios, para escribir los reintentos a mano.