Quando recebemos um projecto para refactorizar, há uma pergunta que pomos sempre na primeira semana: quem é que desenhou o schema, e em que momento? A resposta é quase sempre a mesma. O schema foi desenhado depois das telas. Alguém pegou no Figma, mapeou os ecrãs principais, e só depois — geralmente sob pressão de tempo — escreveu as tabelas para suportarem aquilo que o design pedia.
Isto não é negligência. É o caminho natural quando o cliente vê design, não vê schema, e está a pagar por velocidade. Mas é o motivo número um pelo qual produtos que correm bem no ano um colapsam no ano dois. E é a razão pela qual, em qualquer projecto com mais do que páginas estáticas, construímos primeiro o schema, depois as rotas, e o desenho visual por último.
Este artigo é a explicação técnica do porquê.
Onde os schemas mal pensados doem
Há cinco sítios típicos onde se paga a factura de um modelo de dados mal pensado:
1. Migrações que nunca acabam
Cada relação errada ou cada coluna mal-nomeada vai ter de ser reparada com migrações. As primeiras três ou quatro são triviais. A décima quinta exige downtime. A quadragésima exige um fim-de-semana de pessoas seniores e um runbook escrito em pânico.
2. Queries que ficam impossíveis de escrever
Quando alguma vez tentaste responder a uma pergunta de negócio aparentemente simples — "quantos utilizadores fizeram X depois de Y mas antes de Z?" — e descobriste que a query precisa de seis joins e três sub-queries, é provável que o schema esteja errado. Schemas bem pensados tornam o SQL óbvio. Schemas mal pensados tornam-no folclore.
3. Funcionalidades novas que parecem fáceis e não são
"Adicionar uma segunda morada por utilizador" é trivial num schema bem pensado e doloroso quando "morada" está hard-coded como três colunas na tabela users. A maior parte das "regressões inesperadas" depois de ano um é exactamente isto: features que tropeçam no modelo.
4. Race conditions que aparecem só em produção
Um schema que não pensa em transacções desde o primeiro dia vai ter race conditions. Booking de campos, alocação de stock, pagamentos partilhados — qualquer fluxo onde dois utilizadores podem competir pelo mesmo recurso. Resolver isto a posteriori envolve adicionar locks, retries, semáforos. Cada um deles é uma camada de complexidade que não devia existir.
5. Performance que se degrada com o crescimento
Schemas planos crescem mal. Sem índices certos nas colunas certas, queries que demoram 50ms com mil linhas demoram 5 segundos com cem mil. A solução habitual — adicionar índices reactivamente — funciona mas tarde, e na altura em que a equipa percebe, há já backlog de utilizadores frustrados.
Bom schema é o trabalho menos visível e o mais valioso. Mau schema é dívida com juro composto.
O nosso processo
Para qualquer projecto com lógica real — não para sites institucionais simples, onde o exagero é desproporcional — fazemos isto, por esta ordem, na primeira semana:
Dia 1-2: entender o domínio em palavras
Antes de abrir qualquer ferramenta, sentamo-nos com o cliente e escrevemos em prosa o que cada coisa é, o que pode acontecer-lhe, e como se relaciona com as outras. Para o Field to Play, foi: o que é um campo, o que é uma reserva, o que é uma equipa, o que é um jogo? Que estados pode cada um ter? Que regras de negócio decidem as transições?
Este documento — geralmente uma página de A4 com bullets — é o brief técnico real. Sem ele, qualquer schema é palpite.
Dia 3-5: schema em SQL, não em ferramentas visuais
Escrevemos o schema em SQL puro num ficheiro, com comentários. Não em diagramas. Não em ORMs que escondem decisões.
-- A facility has many courts. A court has different surfaces depending
-- on the weekday (e.g. clay Monday-Friday, synthetic Saturday-Sunday).
CREATE TABLE courts (
id SERIAL PRIMARY KEY,
facility_id INT NOT NULL REFERENCES facilities(id) ON DELETE CASCADE,
name TEXT NOT NULL,
sport TEXT NOT NULL,
surface_default TEXT NOT NULL,
active BOOLEAN DEFAULT TRUE,
UNIQUE (facility_id, name)
);
CREATE TABLE court_surface_exceptions (
court_id INT NOT NULL REFERENCES courts(id) ON DELETE CASCADE,
day_of_week SMALLINT NOT NULL CHECK (day_of_week BETWEEN 0 AND 6),
surface TEXT NOT NULL,
PRIMARY KEY (court_id, day_of_week)
);
SQL puro força-nos a tomar decisões que ferramentas visuais escondem: o que é único, o que cascade-deleta, que constraints existem, que indices são precisos. Aqueles CHECK e UNIQUE constraints são contractos que vamos honrar para sempre.
Dia 6-7: edge cases num documento separado
Antes de qualquer interface, listamos por escrito uma dúzia de casos-limite e validamos que o schema os aguenta. Para um sistema de reserva: o que acontece se dois utilizadores reservarem o mesmo slot ao mesmo segundo? E se a reserva for paga por um terceiro que não é nenhum dos jogadores? E se o campo for fechado por excepção pontual no dia da reserva?
Se um caso-limite não cabe no schema sem alterar tabelas, paramos. Mudamos o schema. Voltamos a validar. Repetimos até que todos os casos-limite caibam naturalmente.
Só depois: rotas e interface
Quando o schema está estável, as rotas são quase mecânicas: um endpoint por mutação, queries por relação. E a interface segue as queries — não o contrário.
O caso concreto do Field to Play
O Field to Play é o exemplo mais recente. Antes de uma única tela ser desenhada, gastámos uma semana a modelar a tabela de disponibilidade.
A pergunta era simples: como representar "o campo X está disponível das 9h às 11h amanhã"? A resposta ingénua é uma tabela de slots. Mas então: como representar reservas recorrentes? Excepções pontuais (feriado num dia que normalmente abre)? Mudanças sazonais nos horários?
O que nasceu foi uma tabela availability_rules separada de availability_exceptions, com uma view materializada available_slots recalculada por cron sempre que uma regra ou excepção mude. Soa burocrático, e é. Mas é por isso que uma pesquisa em quarenta complexos responde em menos de sessenta milissegundos, e cada tentativa de reserva é segura contra race conditions sem precisarmos de locks aplicacionais.
Se tivéssemos começado pelo desenho do calendário, teríamos modelado "um slot é uma linha", o sistema teria funcionado para vinte campos, e a sexta cidade que onboardássemos teria partido tudo.
Quando NÃO seguir esta regra
Há dois casos onde fazer schema-first é exagero:
-
Sites institucionais sem lógica de negócio. Cinco páginas, um formulário de contacto, conteúdo estático. O modelo de dados aqui é "páginas de markdown num CMS". Não precisa do tratamento.
-
MVPs descartáveis para validar interesse de mercado. Se vamos atirar fora em três meses se ninguém usar, modelo aproximado serve. Mas atenção: muitos MVPs sobrevivem por anos. Se há sinal de que vai escalar, paramos e refazemos o schema antes de adicionar a quarta funcionalidade.
O custo de oportunidade
Fazer schema-first acrescenta cerca de uma semana ao calendário inicial de um projecto típico. Para nós, é uma semana óbvia. Para muitos clientes, parece uma semana perdida quando ainda não há nada visível.
A resposta honesta é: essa semana é a única que paga a si própria com juros compostos. Sem ela, o ano dois é uma sequência de incêndios. Com ela, o ano dois é a parte fácil — onde se adicionam features, não se remendam ruínas.
Quando alguém nos pergunta porque é que os nossos projectos são "boring" no ano dois — sem grandes refactorings, sem pânicos, sem migrações dolorosas — a resposta está na primeira semana, antes da primeira tela.