O DBA foi embora e deixou a chave com você
Em 2013, o fluxo era claro. Você recebia um documento — quase sempre um PDF com um DER exportado do Erwin — e implementava. As tabelas já existiam. Os nomes das colunas já tinham sido decididos por alguém que passou os últimos quinze anos brigando por integridade referencial. Se você quisesse um índice, abria um chamado. Se quisesse uma coluna nova, abria um chamado e esperava o comitê de mudanças da quinta-feira.
Aquilo era lento, burocrático e frequentemente irritante. Mas havia ali uma coisa que perdemos sem perceber: existia um adulto na sala cuja única função era proteger os dados de nós.
Hoje, o fluxo é outro. A squad é autônoma. Você provisiona o banco com Terraform, escolhe entre PostgreSQL e DynamoDB numa thread do Slack que dura quinze minutos, define a chave de partição no meio de um refinamento e sobe a migration junto com o PR da feature. O DBA virou SRE de plataforma, ou virou “time de dados” que cuida do lakehouse, ou simplesmente não existe mais no organograma. Ninguém revisa seu modelo. Ninguém pergunta por que aquele campo é VARCHAR(255) guardando um valor monetário.
Nós ganhamos velocidade — muita. E ganhamos também uma responsabilidade que a maioria de nós nunca foi treinada para exercer: traduzir requisito de negócio em estrutura de dados. Não é uma tarefa acessória do desenvolvimento. É a decisão de arquitetura mais duradoura que um engenheiro toma.
A assimetria que ninguém te contou
Refatorar uma classe custa uma tarde. Trocar de framework custa um algumas semanas. Corrigir um modelo de dados errado em produção, com sete anos de histórico, integrações downstream, relatórios regulatórios e três sistemas legados lendo direto do seu banco, custa um programa inteiro — com gerente de projeto, comitê e risco de compliance.
É por isso que a régua da modelagem precisa ser mais alta que a régua do código. Código ruim você reescreve. Dado ruim você carrega.
O pecado original: quando orientação a objetos vira modelagem de dados
Antes dos princípios, precisamos falar de um viés que a nossa formação instalou e que atrapalha ativamente o raciocínio sobre dados.
A maioria de nós aprendeu a modelar objetos antes de aprender a modelar dados. E aprendeu que a classe é o ponto de partida. Então o caminho natural virou: escrevo a entidade, anoto com @Entity, deixo o Hibernate gerar o DDL, e pronto — “o modelo está feito”.
Só que não está. O que existe ali é uma projeção acidental de decisões de código sobre uma estrutura de armazenamento permanente. Ninguém decidiu nada sobre os dados. O schema foi um efeito colateral.
Objetos e dados são coisas diferentes
Comece pelo tempo de vida. Um objeto existe por milissegundos: nasce numa requisição, é serializado, morre no garbage collector. O dado que ele representa existe por décadas — vai sobreviver ao serviço que o criou, à linguagem em que foi escrito e provavelmente à empresa que o gerou na forma atual.
Depois, a identidade. Objeto tem identidade por referência: dois objetos com os mesmos atributos podem ser coisas distintas, e você resolve isso sobrescrevendo equals(). Dado tem identidade por chave, e essa chave precisa ser estável, única e significativa para o mundo externo — porque outros sistemas vão apontar para ela.
Vem então a inversão mais importante, e a que quase ninguém enuncia: objeto existe para esconder estado; dado existe para expor estado. Encapsulamento é a virtude central da orientação a objetos — o mundo lá fora não deve saber como o saldo é calculado. Mas o banco de dados tem a função oposta: ele precisa ser consultável, auditável, agregável por gente que nunca vai ler o seu código. Aplicar reflexo de encapsulamento na modelagem de dados produz esquemas opacos, com campos crípticos1 cujo significado só o serviço dono conhece.
Some a isso a quantidade de consumidores. O seu objeto tem exatamente um consumidor: o seu código. O seu dado tem N — o time de BI, o relatório regulatório, o serviço de antifraude, o pipeline do lakehouse, o analista que vai abrir um cliente SQL às onze da noite para investigar um incidente, e o próximo time daqui a quatro anos. Modelar para um consumidor conhecido é fácil; modelar para consumidores que você nunca vai conhecer exige outro tipo de rigor.
E, por fim, o critério de otimização. Objeto é otimizado para comportamento: coesão, baixo acoplamento, facilidade de mudança. Dado é otimizado para verdade e para perguntas: integridade, rastreabilidade, latência de consulta. São eixos diferentes — e otimizar um deles não melhora o outro por acidente.
O object-relational impedance mismatch não é um detalhe de mapeamento. É um sintoma de que os dois mundos respondem a perguntas diferentes. Quando você deixa o modelo de objetos ditar o modelo de dados, três estragos previsíveis acontecem:
1. O UPDATE destrutivo. Este é o mais caro de todos. Em OO, mutar estado é natural:
// parece limpo. É uma bomba-relógio.
public class Conta {
private BigDecimal saldo;
public void debitar(BigDecimal valor) {
this.saldo = this.saldo.subtract(valor);
}
}Mapeado direto, isso vira UPDATE conta SET saldo = ? WHERE id = ?. E aí, três meses depois, chega uma contestação de MED sobre um Pix de R$ 4.200 e alguém pergunta: “qual era o saldo dessa conta às 14h32 do dia 12?”. A resposta honesta é: não sabemos. Nós sobrescrevemos.
2. A herança que vaza para a tabela. SINGLE_TABLE produz aquela tabela com 60 colunas onde 40 são nulas dependendo da linha. JOINED produz JOINs em toda leitura para servir uma hierarquia que só existe porque queríamos reaproveitar código. Herança é ferramenta de reúso de comportamento. Ela raramente descreve uma verdade do negócio.
3. O modelo anêmico invertido. A classe vira um saco de getters e setters, a regra vaga nos services — e como a classe não tem semântica, a tabela também não tem. Fica um status genérico, um tipo genérico, um valor sem moeda. O domínio desaparece dos dois lados ao mesmo tempo.
Ajuste de mentalidade
Não é “OO é ruim”. É que o modelo de domínio e o modelo de persistência são dois artefatos distintos, com propósitos distintos, e é saudável que eles não sejam idênticos. O objeto responde “o que esse conceito faz?”. O dado responde “o que aconteceu, e como eu provo?”. Quando você aceita que são perguntas diferentes, os cinco princípios abaixo deixam de ser teoria.
Os cinco princípios
O modelo de dados é o modelo do negócio
Fatos são imutáveis; estado é derivado
Modele para as perguntas, não para os dados
Consistência tem fronteiras — e você as desenha
Todo schema evolui; schema-less é uma mentira confortável
Princípio 01: O modelo de dados é o modelo do negócio
Se o pessoal de operações diz “devolução”, o jurídico diz “devolução”, o Bacen diz “devolução” — e a sua tabela chama TB_MOV_CAD_02 com uma flag TP_MOV = 'D' —, você não modelou dados. Você criou um dicionário paralelo que existe na cabeça de três pessoas, das quais duas já pediram demissão. 🙃
Modelagem começa nos substantivos e verbos do domínio, não nos tipos de coluna. E isso não é preciosismo estético: nome é a interface de leitura do seu dado, e ela dura mais que qualquer API.
Lição 1.1 → Nome de coluna é contrato permanente, não variável local
Renomear uma variável é Shift+F6. Renomear uma coluna em produção é: migration, dual-write, atualizar as views, avisar o time de BI, atualizar os DAGs do lakehouse, atualizar o relatório regulatório, descobrir que existe um SELECT * num job COBOL de 2008 e negociar a janela de mudança.
Na prática, ninguém renomeia. O nome errado fica. E o pior: o nome errado ensina errado. Todo desenvolvedor que chegar nos próximos oito anos vai aprender o domínio lendo o seu schema, e vai aprender torto.
Escreva o nome pensando em quem vai ler daqui a cinco anos, não em quem está digitando agora.
Lição 1.2 → Não ache conceitos distintos sob um nome genérico
Este é o erro mais comum e o mais destrutivo em sistemas financeiros. A palavra “transação” parece cobrir tudo — então cria-se tb_transacao com um tipo e um status, e enfia-se o mundo inteiro lá dentro.
Só que num cartão de crédito, quatro coisas absolutamente diferentes acontecem com o mesmo dinheiro:
Autorização — a bandeira reserva limite. Não há dinheiro se movendo. Pode expirar sozinha.
Captura — o lojista confirma a venda. Agora existe uma obrigação.
Liquidação — o dinheiro efetivamente troca de mãos entre instituições, em D+N.
Faturamento — a obrigação entra num ciclo de cobrança do portador.
Cada uma tem ciclo de vida próprio, prazo próprio, efeito contábil próprio e regra de estorno própria. Achatar as quatro em uma tabela com status IN ('AUTORIZADA','CAPTURADA','LIQUIDADA','FATURADA') parece elegante no dia 1 e vira um inferno no dia 400 — porque uma autorização pode ser parcialmente capturada, uma captura pode ser liquidada em duas parcelas de agenda, e o modelo não tem onde colocar isso.
Regra prática: se dois “estados” de uma entidade têm ciclos de vida independentes, cardinalidade diferente entre si, ou donos diferentes no negócio — eles não são estados. São entidades diferentes.
Lição 1.3 → Seu modelo precisa passar no teste do auditor
Pegue o seu DER e mostre para um analista de negócio, um contador ou alguém de compliance. Não explique nada. Se a pessoa não conseguir apontar onde está a devolução de um Pix, onde está o bloqueio judicial e onde está o saldo disponível versus o saldo bloqueado, o modelo falhou — mesmo que o código funcione perfeitamente.
Isso não é sobre documentação bonita. É que num domínio regulado, o seu banco de dados é a evidência. Quando o regulador pedir a trilha de uma operação e a resposta exigir um JOIN de seis tabelas mais o conhecimento tácito de que flag_ctrl = 3 significa “estornado por MED”, você não tem um problema técnico. Tem um problema de negócio com prazo.
Pense comigo — antes de nomear qualquer coisa
Esse nome existe no vocabulário de quem opera o negócio? Se você inventou o termo, provavelmente inventou também o conceito — e conceito inventado não sobrevive à primeira conversa com o jurídico.
Quando eu digo “transação”, o especialista de domínio entende a mesma coisa que eu? Pergunte assim, literalmente: “para você, o que é uma transação aqui?”. Se ele começar a responder com “depende”, você acabou de descobrir que são várias entidades.
Esses dois campos que eu estou juntando têm o mesmo dono e o mesmo ciclo de vida? Se um muda por decisão do cliente e o outro por evento do Bacen, eles não pertencem à mesma linha.
Se eu sumir amanhã, alguém entende esse schema sem me perguntar nada? Essa é a única métrica de qualidade de nomenclatura que importa.
Princípio 02: Fatos são imutáveis; estado é derivado
Este é o princípio mais poderoso do artigo, o mais violado na prática, e o único que a contabilidade resolveu no século XIV — enquanto nós ainda escrevemos UPDATE saldo SET ... em 2026.
Luca Pacioli formalizou as partidas dobradas em 1494 sobre uma ideia simples: você não altera o passado. Você registra um novo lançamento. O razão é append-only, e o saldo nunca é armazenado como verdade — ele é calculado. É a arquitetura de dados mais bem-sucedida da história da humanidade, e ela é anterior ao computador.
Lição 2.1 → O UPDATE destrutivo apaga a única evidência que você tinha
Compare as duas modelagens de uma transferência de R$ 100 entre contas internas.
Modelo mutável (o que a maioria faz):
UPDATE conta SET saldo = saldo - 100 WHERE id = 'A';
UPDATE conta SET saldo = saldo + 100 WHERE id = 'B';Funciona. É rápido. E destruiu a informação. Não existe mais como saber por que o saldo mudou, quando, por ordem de quem, ou qual era antes. Você guardou o resultado e jogou fora a causa.
Modelo de fatos (razão append-only):
id conta natureza valor origem
─────────────────────────────────────────────────
9001 A DEBITO 100,00 TRANSF#7712
9002 B CREDITO 100,00 TRANSF#7712Nada foi sobrescrito. Duas linhas novas, apontando para a mesma ordem de transferência — e a soma das duas é zero, que é exatamente a garantia das partidas dobradas.
-- saldo deixa de ser um campo. Vira uma pergunta.
SELECT SUM(CASE WHEN tipo = 'CREDITO' THEN valor ELSE -valor END)
FROM lancamento
WHERE conta = 'A' AND data_movimento <= :data_corte;Repare no ganho que caiu de graça: aquele :data_corte significa que você consegue responder “qual era o saldo em qualquer instante do passado” — sem nenhuma estrutura adicional. No modelo mutável, essa pergunta é impossível a qualquer preço.
“Mas isso não é lento?”
É a objeção padrão, e ela tem resposta madura: snapshot. Você materializa o saldo consolidado em pontos de corte (fechamento diário, por exemplo) e soma apenas o delta desde o último snapshot. O snapshot é cache, não verdade — pode ser descartado e reconstruído a qualquer momento a partir dos fatos. O caminho errado é o inverso: guardar o saldo como verdade e tentar reconstruir os fatos depois. Isso não tem volta.
Lição 2.2 → O fato precisa carregar o “porquê”, não só o “quanto”
Muitos times chegam na metade do caminho: criam uma tabela de histórico com saldo_anterior e saldo_novo. Isso é melhor que nada, mas ainda não é história — é o diff sem o commit message.
Um fato bem modelado responde: o que aconteceu, quando, por causa de quê, por ordem de quem, e sob qual referência externa.
lancamento
├─ id
├─ conta_id
├─ natureza DEBITO | CREDITO
├─ valor DECIMAL(18,2) — nunca float, nunca varchar
├─ evento_origem PIX_RECEBIDO | DEVOLUCAO_MED | TARIFA_TED | ...
├─ referencia_externa E2E ID do Pix, NSU, ID do contrato
├─ lancamento_estornado_id aponta para o fato que este reverte
├─ data_movimento quando o dinheiro se moveu
├─ data_competencia a que período isso pertence
└─ data_registro quando NÓS soubemosE note a linha de estorno: no modelo de fatos, você nunca apaga nem edita um lançamento errado. Você lança o contrário e aponta para o original. É exatamente o que a contabilidade chama de lançamento de retificação, e é o que permite que a resposta para “esse cliente foi debitado indevidamente?” seja SELECT, e não arqueologia em log de aplicação.
Lição 2.3 → Tempo é dimensão do dado, não metadado de auditoria
Quase todo modelo carrega um created_at e acha que resolveu o tempo. Não resolveu. Em sistemas financeiros existem, no mínimo, três tempos independentes, e confundi-los gera divergência contábil real:
Data de movimento — quando o dinheiro efetivamente se moveu.
Data de competência — a que período o fato pertence economicamente.
Data de registro (ou conhecimento) — quando o seu sistema tomou ciência.
Um exemplo que todo mundo que trabalha com fechamento reconhece: em 3 de agosto, chega uma devolução referente a uma operação de 28 de julho. Movimento: 3/8. Competência: julho. Registro: 3/8. Se o seu modelo tem uma data só, você é obrigado a escolher entre mentir para a contabilidade ou mentir para o extrato do cliente.
E há a pergunta que separa modelo maduro de modelo ingênuo: “me mostre o balancete como ele estava em 31/07, do jeito que nós o víamos naquele dia”. Isso é modelagem bitemporal — tempo do fato × tempo do conhecimento. Se você fez UPDATE, essa pergunta não tem resposta. Se você guardou fatos com as três datas, é um WHERE.
Pense comigo — antes de escrever qualquer UPDATE
Isso que eu vou sobrescrever é um estado ou é um fato? “Endereço do cliente” é estado (talvez). “Saldo”, “status de pagamento”, “limite aprovado” quase sempre são consequências de fatos — e fato não se sobrescreve.
Se alguém perguntar daqui a dois anos por que esse valor mudou, eu consigo responder só com dados? Se a resposta envolve “olha o log da aplicação”, o modelo está incompleto. Log expira; dado permanece.
Eu consigo reconstruir esse valor do zero se ele for corrompido? Se sim, ele é derivado e não deveria ser a fonte da verdade. Se não, ele é fato e não deveria ser mutável. Uma das duas coisas — nunca as duas.
Quantos tempos diferentes esse fato tem? Pergunte ao negócio: “quando isso aconteceu” e “a que mês isso pertence” são a mesma data? Se hesitarem, você precisa de duas colunas.
Princípio 03: Modele para as perguntas, não para os dados
Aqui mora a discussão relacional × não relacional — e ela é muito menos religiosa do que a internet faz parecer. Normalização otimiza escrita e integridade. Desnormalização otimiza leitura. Nenhuma das duas é “certa” no vácuo. A pergunta correta nunca foi “qual banco eu uso?”, e sim “quais perguntas esse dado precisa responder, com que latência, e quantas vezes por segundo?”.
Lição 3.1 → Normalização e desnormalização são estratégias, não moral
A terceira forma normal existe para garantir que cada fato seja afirmado em exatamente um lugar — o que torna impossível ter duas verdades contraditórias. É uma proteção de escrita. Você paga por ela em JOINs.
Desnormalizar é o movimento contrário: aceitar redundância controlada para comprar latência de leitura. Você paga por ela em risco de divergência.
O erro não é escolher um dos dois. O erro é escolher sem saber qual dos dois custos você está pagando. Um sinal claro de que a decisão foi automática: o time normalizou até a última gota uma tabela que só é lida por um dashboard uma vez por dia — e desnormalizou o cadastro de cliente, que é escrito por seis fluxos diferentes.
Lição 3.2 → No NoSQL, o padrão de acesso vem antes da entidade — e é quase irreversível
Esta é a diferença prática que muita gente descobre tarde demais.
No relacional, você modela entidades e relacionamentos primeiro; se uma consulta nova aparecer daqui a um ano, você cria um índice e segue a vida. O modelo perdoa.
Num banco de chave-partição — DynamoDB, Cassandra —, a chave é a arquitetura. Ela determina onde o dado fisicamente mora, e mudá-la depois significa reescrever a tabela inteira. O modelo não perdoa.
// extrato: "os lançamentos da conta X no período Y, mais recentes primeiro"
PK = "CONTA#00219-4"
SK = "LANC#2026-08-19T14:32:11Z#a3f9"
// a pergunta vira uma única leitura sequencial, sem JOIN, sem scan:
Query(PK = "CONTA#00219-4", SK between "LANC#2026-08-01" and "LANC#2026-08-31")Isso é lindo — para essa pergunta. Agora tente responder “todos os lançamentos com E2E ID X, em qualquer conta”, ou “soma de tarifas por agência no mês”. Sem um índice secundário planejado desde o início, essas perguntas custam um scan da base inteira.
Portanto: em NoSQL, listar os padrões de acesso antes de desenhar a chave não é boa prática opcional. É a modelagem inteira.
Lição 3.3 → Um dado pode ter vários modelos — mas só uma fonte da verdade
A saída madura para o falso dilema é aceitar que o mesmo fato bancário quer formas diferentes para consumidores diferentes:
Razão contábil — relacional, normalizado, consistência forte, transacional. É a fonte da verdade. Precisa fechar até o centavo e sobreviver a auditoria.
Extrato do app — documento desnormalizado, particionado por conta e período, leitura em milissegundos, consistência eventual tolerável (o cliente aceita ver o lançamento com 300 ms de atraso; não aceita esperar 4 segundos pela tela).
Base analítica — colunar, orientada a agregação, atualizada por batch.
Três modelos, um fato. Isso é CQRS na prática — e não é sofisticação: é reconhecer que “consultar” e “registrar” são atividades com requisitos opostos.
A regra que não pode ser quebrada
Projeções derivam; nunca escrevem por conta própria. No instante em que dois caminhos de escrita alimentam duas cópias do mesmo fato de forma independente, você não tem duas visões — tem duas verdades. E quando elas divergirem (e vão divergir), não existirá critério técnico para decidir qual está certa.
Pense comigo — antes de escolher a tecnologia
Quais são as cinco perguntas que esse dado vai responder em produção? Escreva as cinco antes de abrir o Terraform. Se você não consegue escrever nem três, você ainda não entendeu o requisito — e nenhum banco resolve isso.
Qual dessas perguntas é feita 10.000 vezes por minuto, e qual é feita uma vez por mês? Modele para a primeira, tolere a segunda. Otimizar o caminho frio é a forma mais comum de desperdiçar complexidade.
Quem é o dono da verdade desse dado? Se você não consegue apontar um lugar, você já tem um problema de consistência — só ainda não sabe.
O que quebra se essa leitura estiver 500 ms atrasada? Se a resposta for “nada”, você acabou de ganhar liberdade arquitetural enorme. Se for “o saldo pode ficar negativo”, você acabou de descobrir onde precisa de consistência forte.
Princípio 04: Consistência tem fronteiras — e você as desenha
Todo engenheiro sabe recitar ACID. Poucos percebem que o desenho do modelo é o que decide onde o ACID pode ser aplicado. A fronteira transacional não é uma configuração de infraestrutura: é uma consequência direta de como você agrupou os dados.
Lição 4.1 → O agregado define o que cabe em um commit
Um agregado é o conjunto de dados que precisa mudar junto para que uma invariante do negócio se mantenha. “O saldo da conta nunca pode ficar abaixo do limite” é uma invariante — logo, conta e seus lançamentos do momento precisam ser consistentes no mesmo instante.
O sintoma de agregado mal desenhado é conhecido: você se pega precisando de uma transação que atravessa três serviços. Nesse ponto há exatamente duas saídas honestas — ou a fronteira está errada e esses dados deveriam viver juntos, ou eles são realmente independentes e você precisa de saga com compensação. O que não existe é a terceira: transação distribuída em dois bancos diferentes via @Transactional e fé.
Lição 4.2 → Estados intermediários são cidadãos de primeira classe
Um Pix atravessa a fronteira da sua instituição. Entre a ordem do cliente e a confirmação do SPI existe um intervalo em que o dinheiro precisa estar contabilmente em algum lugar. Ele não está mais totalmente disponível para o pagador e ainda não é do recebedor.
Modelo ingênuo:
conta.saldo -= valor; // e se o SPI recusar?
conta.processado = true; // processado significa o quê, exatamente?Modelo honesto: o valor sai do saldo disponível e entra numa posição transitória explícita — bloqueio, provisão, conta de trânsito —, e só sai de lá quando existe um fato terminal: liquidação confirmada, devolução, ou expiração de prazo.
Isso muda a modelagem por completo. Você deixa de ter um boolean e passa a ter uma máquina de estados com transições registradas como fatos: ORDEM_RECEBIDA → RESERVADA → ENVIADA_SPI → LIQUIDADA, com ramos para DEVOLVIDA e EXPIRADA. Cada transição é uma linha. O estado atual é a última linha — ou seja, derivado (Princípio 2 outra vez).
Teste rápido no seu modelo: se existe um campo booleano chamado processado, enviado ou ok em algo que atravessa rede, você tem um estado intermediário não modelado. E ele vai aparecer — geralmente às duas da manhã, na forma de dinheiro que “sumiu”.
Lição 4.3 → Idempotência mora no schema, não no if
Sistemas de mensageria sérios entregam at-least-once. Isso significa que a mesma mensagem vai chegar duas vezes — não é hipótese, é garantia. E a defesa contra isso não é código defensivo; é uma restrição de banco.
-- a defesa real cabe em uma linha
ALTER TABLE lancamento
ADD CONSTRAINT uk_idempotencia UNIQUE (chave_idempotencia);Um SELECT antes do INSERT para “verificar se já existe” não protege nada: entre a leitura e a escrita cabem duas instâncias do seu serviço fazendo exatamente a mesma coisa. Só a restrição no banco é atômica. O código apenas trata a violação e devolve sucesso — porque receber duas vezes o mesmo pedido e produzir um efeito só é o comportamento correto.
No Pix, esse pensamento fica evidente: o E2E ID identifica unicamente a operação ponta a ponta. Ele é uma chave natural de idempotência que o próprio arranjo te entrega de graça. Times que a ignoram e criam um UUID interno próprio descobrem, na primeira reapresentação de mensagem, que duplicaram crédito.
Pense comigo — antes de desenhar a fronteira
Qual invariante do negócio essa operação precisa manter no mesmo instante? Só o que a invariante exige entra na transação. Todo o resto pode ser eventual — e provavelmente deveria ser.
Se isso falhar no meio, o dinheiro está em que lugar do meu modelo? Se a resposta for “em lugar nenhum” ou “no ar”, falta uma entidade. Dinheiro em trânsito precisa de endereço.
Se essa mensagem chegar três vezes, quantos efeitos eu produzo? Se a resposta depende de timing, você tem uma condição de corrida esperando volume para se manifestar.
Existe uma chave natural que o domínio já me dá? E2E ID, NSU, número de contrato, chave da nota. Chave natural do domínio quase sempre é melhor guardiã de unicidade do que qualquer UUID que você gerar.
Princípio 05: Todo schema evolui; schema-less é uma mentira confortável
Não existe dado sem schema. Existe schema não declarado. Quando alguém diz “nosso banco é schema-less”, o que aconteceu de fato foi: o schema saiu do banco — onde era validado, versionado e visível — e foi para dentro do código, onde ninguém valida nada e onde existem, agora, quatro versões diferentes rodando ao mesmo tempo em produção.
Lição 5.1 → O schema não sumiu; ele apenas trocou de dono
No relacional, se você tentar inserir texto numa coluna numérica, o banco recusa. É chato e é maravilhoso: o erro acontece na escrita, com o culpado ainda em cena.
Num documento livre, o dado entra. O erro acontece meses depois, na leitura, quando alguém tenta somar uma coleção onde 3% dos registros gravaram valor como string porque uma versão antiga do serviço serializava diferente. Aí o culpado não está mais em cena — e o dado ruim já está em backup, em réplica e no lakehouse.
Escolher NoSQL é legítimo. Escolher NoSQL e abrir mão de validação é abrir mão de qualidade de dado. Use schema validation2, use contratos de dados versionados, use um registry — mas declare o schema em algum lugar onde ele seja verificável por máquina, não pela boa vontade do próximo desenvolvedor.
Lição 5.2 → Migração é prática de engenharia contínua, não evento traumático
O padrão que torna evolução de schema um não-evento é o expand / contract:
Expandir — adicionar a nova estrutura, sempre opcional/nulável. Nada quebra; a versão antiga do código ignora o campo novo.
Escrever nos dois — o serviço passa a gravar no formato antigo e no novo.
Backfill — job idempotente e reprocessável converte o histórico, em lotes, com controle de progresso.
Migrar a leitura — consumidores passam a ler o campo novo, um a um.
Contrair — só então o campo antigo morre.
Cada passo é reversível isoladamente, e em nenhum momento existe uma janela em que o sistema precise ficar fora do ar. Compare com o modelo tradicional — “migration na madrugada de sábado, com rollback plan e todo mundo acordado” — e fica claro qual dos dois escala num time que faz deploy todo dia.
Lição 5.3 → Saiba, no dia 1, quais mudanças serão baratas e quais serão caras
Nem toda evolução custa igual. Vale ter o mapa na cabeça:
Barato: adicionar campo opcional; adicionar índice; adicionar novo valor a um enum aberto.
Médio: tornar um campo obrigatório; extrair uma entidade nova; alterar precisão numérica.
Caro: mudar o tipo de um campo com histórico; mudar a chave de partição; quebrar uma linha em várias; retroagir semântica (”a partir de agora
valorpassa a ser líquido, não bruto”).
O exemplo canônico: um campo valor DECIMAL(18,2) parece perfeito — até o dia em que a instituição opera câmbio, ou cripto, ou pontos de fidelidade. Transformar valor em { montante, moeda } em 400 milhões de registros é um projeto de trimestre, com dual-write, backfill e reconciliação.
Daí uma heurística que envelhece muito bem: todo dado que tem unidade deve carregar a unidade desde o primeiro dia — moeda com valor, fuso com timestamp, base com taxa. Custa uma coluna hoje. Economiza um trimestre depois.
Cuidado com o inverso
Isso não é licença para generalizar tudo “porque um dia pode mudar”. Modelo excessivamente genérico — o famoso EAV, com chave e valor em texto para “flexibilidade” — é a forma mais eficiente já inventada de não modelar nada e transferir o problema para todo mundo que vier depois. Antecipe unidades e dimensões reais do domínio, não hipóteses.
Pense comigo — antes de criar o primeiro campo
Onde esse schema está declarado de forma que uma máquina consiga verificar? Se a resposta é “na classe Java”, pergunte: qual das quatro versões em produção?
Se eu precisar mudar isso com 300 milhões de registros, qual é o plano? Faça a pergunta hoje, enquanto a tabela está vazia e a resposta ainda é barata.
Esse campo tem unidade implícita? Valor sem moeda, data sem fuso, percentual sem base. Toda unidade implícita é uma migração futura já agendada.
Consigo adicionar um campo aqui sem coordenar deploy com mais ninguém? Se não consigo, o acoplamento não está no código — está no dado, e é muito mais caro de desfazer.
O adulto na sala agora é você
Os cinco princípios têm uma raiz comum, e ela é bem simples: dados descrevem a realidade; código apenas a manipula. Por isso o código é descartável e os dados não são. Framework é opinião — e opinião muda. Um lançamento contábil de 2019 é um fato, e fato não muda nunca.
O DBA não vai voltar. Aquele documento com o DER pronto não vai chegar. A responsabilidade de fazer as perguntas certas antes de escrever a primeira linha de DDL caiu no colo de quem escreve o código — e a boa notícia é que ela cabe em quinze minutos de conversa com quem entende do negócio, desde que sejam os quinze minutos certos.
Cartão de bolso — as 15 lições
Nome de coluna é contrato permanente, não variável local.
Não ache conceitos distintos sob um nome genérico.
Seu modelo precisa passar no teste do auditor.
O UPDATE destrutivo apaga a única evidência que você tinha.
O fato carrega o “porquê”, não só o “quanto”.
Tempo é dimensão do dado, não metadado.
Normalizar e desnormalizar são estratégias, não moral.
Em NoSQL, o padrão de acesso vem antes da entidade.
Vários modelos, uma única fonte da verdade.
O agregado define o que cabe em um commit.
Estados intermediários são cidadãos de primeira classe.
Idempotência mora no schema, não no
if.O schema não sumiu; trocou de dono.
Migração é prática contínua, não evento traumático.
Saiba desde o dia 1 quais mudanças serão caras.
Uma última provocação, para levar para o próximo refinamento. Antes de aceitar a próxima história com um status novo, faça a pergunta que resume o artigo inteiro:
“Se alguém precisar provar, daqui a cinco anos, que isso aqui aconteceu exatamente assim — o meu modelo consegue provar sozinho?”
Se a resposta depender do log da aplicação, da memória de alguém do time ou de uma planilha no drive de alguém, o modelo ainda não está pronto. E vale a pena gastar mais quinze minutos hoje — porque o código que você vai escrever depois disso é descartável de qualquer jeito.
algo oculto, misterioso, enigmático ou difícil de compreender.
A flexibilidade dos bancos de dados orientados a documentos (como o MongoDB) é incrível: você começa a modelar sem amarras, itera rápido e entrega funcionalidades sem a dor de cabeça inicial de migrações complexas. Mas essa liberdade tem um preço. Conforme o sistema cresce, múltiplos microsserviços ou desenvolvedores começam a escrever na mesma base, e a ausência de regras rígidas pode transformar sua coleção em um verdadeiro faroeste de dados inconsistentes.
É aí que entra o Schema Validation.
Trata-se de um contrato definido diretamente no banco que estabelece regras claras sobre como os documentos devem ser estruturados antes de serem persistidos. Em vez de depender exclusivamente da camada de aplicação para garantir que nada saia dos trilhos, o próprio banco rejeita gravações que violem os padrões definidos.
O que o Schema Validation resolve no dia a dia
Campos obrigatórios: Garante que atributos essenciais nunca fiquem nulos ou ausentes em documentos críticos.
Tipagem estrita: Evita bugs silenciosos de coerção de tipo (como receber uma
stringonde a aplicação espera umdecimalouint).Enums e valores permitidos: Restringe o preenchimento de campos de status e categorias a listas pré-aprovadas.
Bloqueio de campos não previstos: Impede que dados acidentais ou formatos antigos poluam novos registros (
additionalProperties: false).
Na prática, o Schema Validation une o melhor de dois mundos: mantém a agilidade do modelo orientado a documentos durante o desenvolvimento inicial e permite “travar” o modelo quando ele atinge maturidade arquitetural, garantindo governança e integridade sem perder a flexibilidade do NoSQL.

