Diseñando una Plataforma de E-Commerce
Las plataformas de comercio electrónico como Amazon, Shopify y eBay deben manejar catálogos masivos de productos, sesiones de compra concurrentes, rastreo de inventario en tiempo real y enormes picos de tráfico durante eventos como Black Friday. El diseño del sistema debe priorizar la disponibilidad, la consistencia de datos para inventario y pedidos, y la búsqueda de baja latencia.
Requisitos de Escala
- 100 millones+ de productos en el catálogo
- 50 millones de usuarios activos diarios
- 10,000+ pedidos por segundo durante pico
- 99.99% de disponibilidad para navegación y búsqueda
- Consistencia fuerte para inventario y pagos
- Carga de página sub-200ms para páginas de productos
Diseño del Catálogo de Productos
El catálogo de productos es la base de cualquier sistema de comercio electrónico. Debe soportar tipos de productos diversos con atributos variados, búsqueda rápida y navegación eficiente a través de categorías.
// Flexible product schema supporting varied attributes
interface Product {
id: string;
sellerId: string;
title: string;
slug: string;
description: string;
category: CategoryPath; // e.g., ["Electronics", "Phones", "Smartphones"]
brand: string;
basePrice: number;
currency: string;
images: ProductImage[];
variants: ProductVariant[];
attributes: Record<string, string | number | boolean>;
status: "DRAFT" | "ACTIVE" | "ARCHIVED";
createdAt: Date;
updatedAt: Date;
}
interface ProductVariant {
sku: string;
attributes: Record<string, string>; // e.g., { color: "Red", size: "M" }
price: number;
inventoryCount: number;
weight: number;
dimensions: { length: number; width: number; height: number };
}
type CategoryPath = string[];
// Stored as: "Electronics > Phones > Smartphones"
// Enables efficient queries: find all products under "Electronics"Para almacenamiento, usa PostgreSQL para los datos relacionales de productos y Elasticsearch para búsqueda de texto completo y filtrado por facetas. Mantenlos sincronizados con un pipeline CDC (Change Data Capture) o actualizaciones basadas en eventos.
Arquitectura del Carrito de Compras
El carrito de compras debe ser rápido, resistente y manejar acceso concurrente. Hay dos enfoques principales: carritos del lado del servidor y carritos del lado del cliente.
| Enfoque | Pros | Contras |
|---|---|---|
| Del lado del servidor (Redis/BD) | Persiste entre dispositivos, soporta analítica, permite recuperación de carrito abandonado | Mayor carga del servidor, latencia en cada operación |
| Del lado del cliente (localStorage) | Cero carga del servidor, operaciones instantáneas, funciona offline | Se pierde al limpiar el navegador, sin sincronización entre dispositivos |
| Híbrido | Lo mejor de ambos mundos, local para velocidad, servidor para persistencia | Complejidad de lógica de sincronización, resolución de conflictos necesaria |
interface CartItem {
productId: string;
variantSku: string;
quantity: number;
priceAtAdd: number;
addedAt: Date;
}
interface ShoppingCart {
userId: string;
items: CartItem[];
updatedAt: Date;
expiresAt: Date;
}
class CartService {
private redis: RedisClient;
async addItem(userId: string, item: CartItem): Promise<ShoppingCart> {
const cartKey = `cart:${userId}`;
const available = await inventoryService.checkAvailability(
item.variantSku,
item.quantity
);
if (!available) throw new Error("Item out of stock");
const cart = await this.getCart(userId);
const existingIndex = cart.items.findIndex(
(i) => i.variantSku === item.variantSku
);
if (existingIndex >= 0) {
cart.items[existingIndex].quantity += item.quantity;
} else {
cart.items.push(item);
}
cart.updatedAt = new Date();
cart.expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
await this.redis.setex(cartKey, 7 * 86400, JSON.stringify(cart));
return cart;
}
}Gestión de Inventario
La gestión de inventario es uno de los aspectos más desafiantes del comercio electrónico. El sistema debe prevenir la sobreventa mientras maximiza la disponibilidad. Esto requiere control de concurrencia cuidadoso.
El Problema de la Sobreventa
Si 100 usuarios intentan comprar el último artículo simultáneamente, implementaciones ingenuas podrían dejar que los 100 tengan éxito. Debes usar operaciones atómicas o bloqueo pesimista para prevenir esto. El enfoque más común es reservar inventario al momento del checkout y liberarlo si el pedido no se completa dentro de un timeout.
class InventoryService {
async reserveInventory(
items: { sku: string; quantity: number }[],
orderId: string,
ttlMinutes: number = 15
): Promise<boolean> {
const client = await db.pool.connect();
try {
await client.query("BEGIN");
for (const item of items) {
const result = await client.query(
`UPDATE inventory
SET available_count = available_count - $1,
reserved_count = reserved_count + $1
WHERE sku = $2
AND available_count >= $1
RETURNING available_count`,
[item.quantity, item.sku]
);
if (result.rowCount === 0) {
await client.query("ROLLBACK");
await this.releaseReservation(orderId);
return false;
}
await client.query(
`INSERT INTO inventory_reservations (order_id, sku, quantity, expires_at)
VALUES ($1, $2, $3, NOW() + INTERVAL '$4 minutes')`,
[orderId, item.sku, item.quantity, ttlMinutes]
);
}
await client.query("COMMIT");
return true;
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
async releaseExpiredReservations(): Promise<void> {
const expired = await db.query(
`SELECT * FROM inventory_reservations WHERE expires_at < NOW()`
);
for (const reservation of expired.rows) {
await db.query(
`UPDATE inventory
SET available_count = available_count + $1,
reserved_count = reserved_count - $1
WHERE sku = $2`,
[reservation.quantity, reservation.sku]
);
await db.query(
`DELETE FROM inventory_reservations WHERE id = $1`,
[reservation.id]
);
}
}
}Pipeline de Procesamiento de Pedidos
El procesamiento de pedidos es un pipeline asíncrono de múltiples pasos. Usar un enfoque dirigido por eventos asegura fiabilidad y permite que cada paso se reintente independientemente.
enum OrderStatus {
CREATED = "CREATED",
PAYMENT_PENDING = "PAYMENT_PENDING",
PAYMENT_CONFIRMED = "PAYMENT_CONFIRMED",
PREPARING = "PREPARING",
SHIPPED = "SHIPPED",
DELIVERED = "DELIVERED",
CANCELLED = "CANCELLED",
REFUNDED = "REFUNDED",
}
async function handleOrderCreated(order: Order): Promise<void> {
// 1. Validate order
await validateOrder(order);
// 2. Reserve inventory
const reserved = await inventoryService.reserveInventory(
order.items.map((i) => ({ sku: i.sku, quantity: i.quantity })),
order.id
);
if (!reserved) {
await updateOrderStatus(order.id, OrderStatus.CANCELLED, "Out of stock");
await notifyCustomer(order.customerId, "order_cancelled_stock");
return;
}
// 3. Process payment
await updateOrderStatus(order.id, OrderStatus.PAYMENT_PENDING);
const paymentResult = await paymentService.charge(order);
if (!paymentResult.success) {
await inventoryService.releaseReservation(order.id);
await updateOrderStatus(order.id, OrderStatus.CANCELLED, "Payment failed");
return;
}
// 4. Confirm order
await updateOrderStatus(order.id, OrderStatus.PAYMENT_CONFIRMED);
await inventoryService.confirmReservation(order.id);
// 5. Send to fulfillment
await fulfillmentService.createShipment(order);
await notifyCustomer(order.customerId, "order_confirmed");
}Búsqueda y Recomendaciones
La búsqueda de productos y las recomendaciones son críticas para la conversión. La búsqueda debe soportar consultas de texto completo, filtrado por facetas (marca, rango de precio, calificaciones), autocompletado y tolerancia a errores tipográficos.
- Motor de búsqueda: Elasticsearch u OpenSearch para búsqueda de texto completo con puntuación de relevancia
- Autocompletado: Usar tokenizadores edge n-gram para coincidencia de prefijos mientras el usuario escribe
- Búsqueda por facetas: Consultas de agregación en Elasticsearch para filtros como rangos de precio y categorías
- Personalización: Reordenar resultados de búsqueda basándose en historial y preferencias del usuario
- Recomendaciones: Filtrado colaborativo ("clientes que compraron X también compraron Y") y filtrado basado en contenido (atributos similares de producto)
Escalado para Alto Tráfico (Black Friday)
Los eventos pico de compras pueden ver 10-100x el tráfico normal. El sistema debe estar preparado con anticipación.
| Estrategia | Implementación |
|---|---|
| CDN para assets estáticos | Cachear imágenes de productos, CSS, JS en CloudFront/Cloudflare edge |
| Réplicas de lectura | Escalar consultas de catálogo de lectura intensiva entre múltiples réplicas de BD |
| Procesamiento de pedidos basado en colas | Almacenar pedidos en buffer en una cola para que el backend procese a un ritmo sostenible |
| Auto-escalado | Pre-calentar instancias antes del evento, auto-escalar basándose en CPU y conteo de solicitudes |
| Circuit breakers | Degradar funciones no esenciales (recomendaciones, reseñas) bajo carga extrema |
| Rate limiting | Proteger contra bots y abuso con límites de tasa por usuario |
Resumen de Arquitectura
Una plataforma de e-commerce bien diseñada separa responsabilidades en servicios independientes (catálogo, carrito, inventario, pedidos, pagos, búsqueda), usa caché agresivamente para cargas de lectura intensiva, asegura consistencia fuerte solo donde se requiere (inventario y pagos), y se degrada elegantemente bajo carga extrema. La idea clave es que la navegación y búsqueda pueden tolerar consistencia eventual, mientras que las operaciones de inventario y pago deben ser fuertemente consistentes.