Diseño de Bases de Datos12 Mar 202610 min de lectura

Paso a Paso: Construyendo un Schema de Base de Datos de E-commerce desde Cero

Diseñar la base de datos para una aplicación de e-commerce es uno de los ejercicios más instructivos en diseño de bases de datos. Esta guía construye un schema completo con usuarios, productos, variantes, pedidos, pagos, carrito y más — con decisiones de diseño explicadas.

Diseñar la base de datos para una aplicación de e-commerce es uno de los ejercicios de diseño de base de datos más comunes y más instructivos. Ya sea que estés construyendo un competidor de Shopify, un marketplace, o una simple tienda online, el modelo de datos central sigue patrones que todo desarrollador debe entender.

En esta guía, construiremos un schema de e-commerce completo desde cero — comenzando con las entidades principales y progresivamente agregando las características que las tiendas reales necesitan. Al final, tendrás un modelo de datos listo para producción con relaciones propias, indexes, y decisiones de diseño explicadas.

Las Entidades Centrales

Toda aplicación de e-commerce necesita estas tablas fundamentales. Comencemos aquí y construyamos hacia afuera.

Users

La tabla users es la fundación. Como mínimo, necesitas un id (primary key), email (único, para autenticación), password_hash (nunca almacenes contraseñas en texto plano), name, created_at, y updated_at. Dependiendo de tu estrategia de auth, podrías también incluir email_verified_at, remember_token, y avatar_url.

Una decisión de diseño clave: ¿deberías separar datos de autenticación de datos de perfil? Para aplicaciones simples, mantenerlos en una tabla está bien. Para sistemas más grandes, dividir en users (auth) y profiles (display data) te da más flexibilidad — especialmente si más adelante agregas proveedores OAuth o SSO.

Products

Los productos necesitan un id, name, slug (versión amigable para URL del nombre), description (texto/HTML para contenido enriquecido), price (almacenado en centavos como entero para evitar problemas de punto flotante), compare_at_price (para mostrar descuentos), sku, stock_quantity, is_active (booleano para controlar visibilidad), weight y dimensions (para cálculos de envío), y timestamps.

Almacena precios como enteros en la unidad monetaria más pequeña (centavos para USD, peniques para GBP). Usar DECIMAL(10,2) también funciona, pero la aritmética de enteros elimina sorpresas de punto flotante completamente. Cualquiera que sea tu elección, sé consistente en todas las tablas.

Categories

Las categorías típicamente usan una estructura autorreferenciada: id, name, slug, description, parent_id (foreign key a la misma tabla, nullable para categorías raíz), y sort_order. El patrón parent_id crea una estructura de árbol — "Electrónica > Teléfonos > Smartphones" — sin necesidad de una tabla separada para cada nivel.

Los productos y categorías tienen una relación many-to-many (un producto puede estar en múltiples categorías, y una categoría contiene muchos productos), por lo que necesitarás una tabla de unión: product_categories con product_id y category_id.

Orders

La tabla orders captura la transacción: id, user_id (foreign key), status (enum: pending, processing, shipped, delivered, cancelled, refunded), subtotal, tax, shipping_cost, total, currency, shipping_address_id, billing_address_id, notes, placed_at, y timestamps.

Observa que almacenamos los totales calculados (subtotal, tax, total) directamente en la orden en lugar de calcularlos desde line items cada vez. Esto es intencional — los precios, impuestos, y descuentos pueden cambiar después de que se coloca una orden, pero el registro de orden debe reflejar lo que el cliente realmente pagó.

Order Items (Line Items)

Aquí es donde las órdenes se conectan a productos: id, order_id (foreign key), product_id (foreign key), variant_id (foreign key, nullable), quantity, unit_price, total_price, product_name, product_sku.

Decisión de diseño crítica: almacenamos product_name, product_sku, y unit_price directamente en el order item, aunque también referenciamos product_id. ¿Por qué? Porque los productos cambian. Un producto podría ser renombrado, repreciado, o eliminado después de que se coloca una orden. El order item debe preservar lo que el cliente realmente compró en el momento de la compra. El foreign key de product_id es útil para analytics y vinculación, pero los campos desnormalizados son la fuente de verdad para la orden.

Agregando Variantes de Producto

La mayoría de las tiendas de e-commerce reales necesitan variantes de producto — una camiseta viene en diferentes tamaños y colores, una laptop viene con diferentes configuraciones de RAM y almacenamiento.

Product Variants

Crea una tabla product_variants: id, product_id (foreign key), sku (único por variante), price (sobrescribe el precio del producto si se establece), stock_quantity, weight, is_active, y timestamps.

Variant Options

Para los atributos en sí (Size: S/M/L/XL, Color: Red/Blue/Green), necesitas dos tablas. Una tabla option_types almacena los nombres de atributos: id, name (como "Size" o "Color"), sort_order. Una tabla option_values almacena los valores posibles: id, option_type_id (foreign key), value (como "Large" o "Red"), sort_order.

Luego una tabla de unión variant_option_values conecta variantes a sus valores de opción: variant_id y option_value_id. Una variante "Large Red T-Shirt" tendría dos entradas en esta tabla — una vinculándola a "Large" y una vinculándola a "Red."

Direcciones

Los usuarios necesitan direcciones de envío y facturación, y podrían tener múltiples de cada una.

Crea una tabla addresses: id, user_id (foreign key), type (enum: shipping, billing, or both), is_default (booleano), first_name, last_name, address_line_1, address_line_2, city, state, postal_code, country_code (ISO 3166-1 alpha-2), phone, y timestamps.

Las órdenes referencian direcciones vía shipping_address_id y billing_address_id. Como con order items, considera si guardar los datos de dirección en la orden (en caso de que el usuario después actualice su dirección) o depender de la referencia de foreign key. Por cumplimiento y precisión, guardar un snapshot es más seguro — ya sea copiando campos a la orden o haciendo direcciones inmutables (creando un nuevo registro en lugar de actualizar).

Payments

La tabla payments rastrea intentos de pago y resultados: id, order_id (foreign key), payment_method (enum: credit_card, paypal, stripe, etc.), gateway_transaction_id (el ID de Stripe, PayPal, etc.), amount, currency, status (enum: pending, authorized, captured, failed, refunded), paid_at, metadata (JSONB para datos específicos de gateway), y timestamps.

Una orden puede tener múltiples registros de pago — un intento fallido seguido de uno exitoso, o un reembolso parcial. Esta es la razón por la que los pagos son una tabla separada en lugar de columnas en la orden.

Shopping Cart

Hay dos enfoques comunes para el diseño del carrito. Carros respaldados por base de datos almacenan datos del carrito en tu base de datos: una tabla carts (id, user_id nullable para carros de invitados, session_id para carros de invitados, timestamps) y una tabla cart_items (id, cart_id, product_id, variant_id, quantity, timestamps). Este enfoque soporta carros persistentes (el usuario agrega artículos en móvil, los ve en desktop) y te permite analizar carros abandonados.

Carros basados en sesión almacenan datos del carrito en la sesión del usuario o local storage y solo crean registros de base de datos al checkout. Esto es más simple pero pierde la capacidad de rastrear y recuperar carros abandonados.

Para la mayoría de aplicaciones de e-commerce, los carros respaldados por base de datos valen la complejidad extra porque la recuperación de carros abandonados es un impulsor de ingresos significativo.

Reviews y Ratings

Una tabla reviews: id, product_id (foreign key), user_id (foreign key), rating (entero 1-5), title, body, is_verified_purchase (booleano — ¿este usuario realmente compró este producto?), is_approved (booleano — para moderación), helpful_count, y timestamps.

Agrega una restricción única en (product_id, user_id) para prevenir reseñas duplicadas. La bandera is_verified_purchase se completa verificando si el usuario tiene una orden completada que contiene este producto.

Coupons y Discounts

Una tabla coupons: id, code (único), type (enum: percentage, fixed_amount, free_shipping), value (el monto de descuento o porcentaje), minimum_order_amount, maximum_discount_amount (límite para descuentos de porcentaje), usage_limit (total de usos permitidos), usage_count (usos actuales), per_user_limit, starts_at, expires_at, is_active, y timestamps.

Una tabla coupon_usages rastrea quién usó qué: id, coupon_id, user_id, order_id, discount_amount, used_at. Esto te permite reforzar límites por usuario y auditar uso de descuentos.

Para cupones específicos de producto, agrega una tabla de unión coupon_products con coupon_id y product_id.

Wishlists

Simple pero valioso: una tabla wishlists con id, user_id, product_id, variant_id (nullable), y created_at. Agrega una restricción única en (user_id, product_id, variant_id) para prevenir duplicados.

Indexes que No Deberías Olvidar

Más allá de los indexes automáticos en primary keys y foreign keys, agrega indexes en products.slug (único, usado en URLs), products.sku (único, usado en admin), products.is_active (filtrado frecuentemente), orders.user_id (usuarios viendo sus órdenes), orders.status (filtrado de admin), orders.placed_at (reporting, ordenamiento), order_items.product_id (reporting de ventas), reviews.product_id (visualización de página de producto), coupons.code (único, buscado al checkout), y addresses.user_id (libreta de direcciones del usuario).

Para PostgreSQL, considera indexes parciales: CREATE INDEX idx_active_products ON products(id) WHERE is_active = true — esto crea un index más pequeño y más rápido que solo cubre productos activos.

Resumen del Schema Completo

Al final, tu schema de e-commerce tiene aproximadamente 15-18 tablas. El flujo de transacción principal es: User → Cart → Order → OrderItems → Payment. Las entidades de apoyo se ramifican desde allí: Products con Variants, Categories, Reviews, Addresses, Coupons.

Las relaciones forman una jerarquía clara. Los Users se sientan en el centro, conectándose a Orders, Addresses, Reviews, Wishlists, y Carts. Los Products se conectan a Categories (many-to-many), Variants, Reviews, y OrderItems. Las Orders se conectan a OrderItems, Payments, y Addresses.

Construyendo Esto en ER Flow

Puedes crear todo este schema visualmente en ER Flow. Comienza creando las entidades principales (Users, Products, Orders, OrderItems), luego progresivamente agrega tablas de apoyo. Usa la agrupación visual de ER Flow para organizar tablas por dominio (core commerce, user management, product catalog, etc.) y coloréalas para claridad.

Una vez que tu schema esté completo, usa el sistema de checkpoint de ER Flow para hacer snapshot del diseño, luego genera migrations para tu base de datos objetivo (PostgreSQL, MySQL) o framework (Laravel, Phinx).

Si estás usando un asistente de IA de codificación como Cursor, puedes describir cada sección de este schema en lenguaje natural y dejar que la IA cree las tablas a través del MCP Server de ER Flow — observándolas aparecer en tu canvas en tiempo real.