Passo a Passo: Construindo um Schema de Banco de Dados de E-Commerce do Zero
Projete um schema de banco de dados de e-commerce completo — produtos, variantes, pedidos, pagamentos, carrinhos e cupons — com decisões de design explicadas. Um guia prático para lojas online reais.
Projetar o banco de dados para uma aplicação de e-commerce é um dos exercícios de design de banco de dados mais comuns e instrutivos. Se você está construindo um concorrente do Shopify, um marketplace ou uma simples loja online, o modelo de dados central segue padrões que todo desenvolvedor deveria entender.
Neste guia, vamos construir um schema de e-commerce completo do zero — começando com as entidades centrais e progressivamente adicionando os recursos que lojas reais precisam. Ao final, você terá um modelo de dados pronto para produção com relacionamentos apropriados, indexes e decisões de design explicadas.
As Entidades Centrais
Toda aplicação de e-commerce precisa dessas tabelas fundamentais. Vamos começar aqui e construir a partir daí.
Users
A tabela users é a base. No mínimo, você precisa de um id (primary key), email (único, para autenticação), password_hash (nunca armazene senhas em texto plano), name, created_at e updated_at. Dependendo da sua estratégia de auth, você pode também incluir email_verified_at, remember_token e avatar_url.
Uma decisão chave de design: você deveria separar dados de autenticação de dados de perfil? Para aplicações simples, mantê-los em uma tabela está fine. Para sistemas maiores, dividir em users (auth) e profiles (dados de exibição) oferece mais flexibilidade — especialmente se você depois adicionar provedores OAuth ou SSO.
Products
Products precisam de um id, name, slug (versão amigável para URL do nome), description (texto/HTML para conteúdo rico), price (armazenado em centavos como um inteiro para evitar problemas com ponto flutuante), compare_at_price (para mostrar descontos), sku, stock_quantity, is_active (boolean para controlar visibilidade), weight e dimensions (para cálculos de entrega) e timestamps.
Armazene preços como inteiros na menor unidade de moeda (centavos para USD, pence para GBP). Usar DECIMAL(10,2) também funciona, mas matemática com inteiros elimina surpresas com ponto flutuante completamente. Qualquer que você escolha, seja consistente em todas as tabelas.
Categories
Categories tipicamente usam uma estrutura auto-referenciada: id, name, slug, description, parent_id (foreign key para a mesma tabela, nullable para categorias raiz) e sort_order. O padrão parent_id cria uma estrutura de árvore — "Eletrônicos > Telefones > Smartphones" — sem precisar de uma tabela separada para cada nível.
Products e categories têm um relacionamento muitos-para-muitos (um product pode estar em múltiplas categories, e uma category contém muitos products), então você precisa de uma junction table: product_categories com product_id e category_id.
Orders
A tabela orders captura a transação: 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 e timestamps.
Repare que armazenamos os totais computados (subtotal, tax, total) diretamente na order em vez de calculá-los a partir de line items toda vez. Isso é intencional — preços, impostos e descontos podem mudar depois de uma order ser colocada, mas o registro de order deveria refletir o que o cliente realmente pagou.
Order Items (Line Items)
É aqui que orders se conectam a products: id, order_id (foreign key), product_id (foreign key), variant_id (foreign key, nullable), quantity, unit_price, total_price, product_name, product_sku.
Decisão crítica de design: armazenamos product_name, product_sku e unit_price diretamente no order item, mesmo que também tenhamos referência a product_id. Por quê? Porque products mudam. Um product pode ser renomeado, reproçado ou deletado depois de uma order ser colocada. O order item deve preservar o que o cliente realmente comprou no momento da compra. A foreign key product_id é útil para analytics e linking, mas os campos desnormalizados são a fonte da verdade para a order.
Adicionando Product Variants
A maioria das lojas de e-commerce reais precisam de product variants — uma camiseta vem em diferentes tamanhos e cores, um laptop vem com diferentes configurações de RAM e armazenamento.
Product Variants
Crie uma tabela product_variants: id, product_id (foreign key), sku (único por variant), price (sobrescreve product price se definido), stock_quantity, weight, is_active e timestamps.
Variant Options
Para os atributos em si (Size: P/M/G/GG, Color: Vermelho/Azul/Verde), você precisa de duas tabelas. Uma tabela option_types armazena os nomes dos atributos: id, name (como "Size" ou "Color"), sort_order. Uma tabela option_values armazena os valores possíveis: id, option_type_id (foreign key), value (como "Large" ou "Red"), sort_order.
Depois uma junction table variant_option_values conecta variants aos seus option values: variant_id e option_value_id. Uma variante "Large Red T-Shirt" teria duas entradas nesta tabela — uma linking-a para "Large" e uma para "Red."
Addresses
Users precisam de endereços de entrega e cobrança, e podem ter múltiplos de cada.
Crie uma tabela addresses: id, user_id (foreign key), type (enum: shipping, billing, ou ambos), is_default (boolean), first_name, last_name, address_line_1, address_line_2, city, state, postal_code, country_code (ISO 3166-1 alpha-2), phone e timestamps.
Orders referenciam addresses via shipping_address_id e billing_address_id. Como com order items, considere se deveria fazer snapshot dos dados de address na order (em caso de o user depois atualizar seu endereço) ou contar com a referência de foreign key. Para compliance e precisão, fazer snapshot é mais seguro — ou copiando campos para a order ou tornando addresses imutáveis (criando um novo registro em vez de atualizar).
Payments
A tabela payments rastreia tentativas de pagamento e resultados: id, order_id (foreign key), payment_method (enum: credit_card, paypal, stripe, etc.), gateway_transaction_id (o ID do Stripe, PayPal, etc.), amount, currency, status (enum: pending, authorized, captured, failed, refunded), paid_at, metadata (JSONB para dados específicos do gateway) e timestamps.
Uma order pode ter múltiplos registros de payment — uma tentativa falhada seguida por uma bem-sucedida, ou um reembolso parcial. É por isso que payments é uma tabela separada em vez de colunas na order.
Shopping Cart
Há duas abordagens comuns ao design de cart. Carts apoiados em banco de dados armazenam dados de cart no seu banco de dados: uma tabela carts (id, user_id nullable para guest carts, session_id para guest carts, timestamps) e uma tabela cart_items (id, cart_id, product_id, variant_id, quantity, timestamps). Essa abordagem suporta carts persistentes (user adiciona items no mobile, os vê no desktop) e permite que você analise abandoned carts.
Carts baseados em sessão armazenam dados de cart na sessão do user ou local storage e só criam registros de banco de dados no checkout. Isso é mais simples mas perde a habilidade de rastrear e recuperar abandoned carts.
Para a maioria das aplicações de e-commerce, carts apoiados em banco de dados valem a complexidade extra porque abandoned cart recovery é um driver de receita significante.
Reviews e Ratings
Uma tabela reviews: id, product_id (foreign key), user_id (foreign key), rating (inteiro 1-5), title, body, is_verified_purchase (boolean — esse user realmente comprou esse product?), is_approved (boolean — para moderação), helpful_count e timestamps.
Adicione um unique constraint em (product_id, user_id) para prevenir reviews duplicadas. O flag is_verified_purchase é populado checando se o user tem uma order completada contendo esse product.
Coupons e Discounts
Uma tabela coupons: id, code (único), type (enum: percentage, fixed_amount, free_shipping), value (o valor de desconto ou percentual), minimum_order_amount, maximum_discount_amount (cap para descontos percentuais), usage_limit (uses totais permitidos), usage_count (uses atuais), per_user_limit, starts_at, expires_at, is_active e timestamps.
Uma tabela coupon_usages rastreia quem usou o quê: id, coupon_id, user_id, order_id, discount_amount, used_at. Isso permite que você enforce per-user limits e audit uso de desconto.
Para coupons específicos de product, adicione uma junction table coupon_products com coupon_id e product_id.
Wishlists
Simples mas valioso: uma tabela wishlists com id, user_id, product_id, variant_id (nullable) e created_at. Adicione um unique constraint em (user_id, product_id, variant_id) para prevenir duplicatas.
Indexes Que Você Não Deveria Esquecer
Além dos indexes automáticos em primary keys e foreign keys, adicione indexes em products.slug (unique, usado em URLs), products.sku (unique, usado no admin), products.is_active (filtrado frequentemente), orders.user_id (users vendo suas orders), orders.status (admin filtrando), orders.placed_at (reporting, sorting), order_items.product_id (sales reporting), reviews.product_id (exibição de página de product), coupons.code (unique, procurado no checkout) e addresses.user_id (livro de endereços do user).
Para PostgreSQL, considere partial indexes: CREATE INDEX idx_active_products ON products(id) WHERE is_active = true — isso cria um index menor e mais rápido que cobre apenas products ativas.
A Visão Geral Completa do Schema
No final, seu schema de e-commerce tem aproximadamente 15-18 tabelas. O fluxo de transação central é: User → Cart → Order → OrderItems → Payment. Entidades de suporte se ramificam a partir daí: Products com Variants, Categories, Reviews, Addresses, Coupons.
Os relacionamentos formam uma hierarquia clara. Users ficam no centro, conectando-se a Orders, Addresses, Reviews, Wishlists e Carts. Products se conectam a Categories (muitos-para-muitos), Variants, Reviews e OrderItems. Orders se conectam a OrderItems, Payments e Addresses.
Construindo Isso em ER Flow
Você pode criar esse schema completo visualmente em ER Flow. Comece criando as entidades centrais (Users, Products, Orders, OrderItems), depois progressivamente adicione supporting tables. Use agrupamento visual do ER Flow para organizar tables por domínio (commerce central, gerenciamento de user, catálogo de products, etc.) e color-code-as para clareza.
Uma vez que seu schema está completo, use o sistema de checkpoint do ER Flow para fazer snapshot do design, depois gere migrations para seu banco de dados alvo (PostgreSQL, MySQL) ou framework (Laravel, Phinx).
Se você estiver usando um assistente de codificação com IA como Cursor, você pode descrever cada seção desse schema em linguagem natural e deixar a IA criar as tables através do MCP Server do ER Flow — vendo-as aparecer no seu canvas em tempo real.