O que é o German Tank Problem aplicado a IDs

Durante a Segunda Guerra Mundial, os Aliados precisavam saber quantos tanques a Alemanha produzia. Em vez de confiar só em espionagem, estatísticos analisaram os números de série dos tanques capturados e estimaram a produção total com uma precisão impressionante. Esse método ficou conhecido como German Tank Problem.

O mesmo raciocínio se aplica ao seu sistema. Se o seu banco de dados usa IDs sequenciais (1, 2, 3...), qualquer pessoa que crie duas contas com alguns dias de diferença consegue estimar quantos usuários você ganhou no período. O ID virou um vazamento silencioso de informação de negócio.

Por isso a comunidade de desenvolvimento recomenda o UUID (Universally Unique Identifier) como identificador público. Ele não revela ordem, volume nem ritmo de crescimento, e ainda dificulta ataques que dependem de adivinhar o próximo ID.

Como funciona um UUID

Um UUID é um identificador de 128 bits, normalmente exibido como 36 caracteres no formato 8-4-4-4-12, por exemplo 550e8400-e29b-41d4-a716-446655440000. São 32 dígitos hexadecimais separados por quatro hifens.

O espaço de valores possíveis é gigantesco. Com 122 bits efetivamente aleatórios na versão 4, a chance de duas máquinas gerarem o mesmo valor é tão baixa que, na prática, tratamos como impossível. Por isso você pode gerar UUIDs no próprio código, sem pedir o próximo número ao banco.

O padrão atual é a RFC 9562, publicada em 2024, que substitui a antiga RFC 4122. Ela define várias versões, cada uma com uma estratégia diferente para montar esses 128 bits.

Principais recursos e versões do UUID

Nem todo UUID é igual. As versões mais usadas hoje são:

  • UUIDv4: quase totalmente aleatório. É o mais comum e o mais simples de adotar, ótimo para identificadores públicos.
  • UUIDv7: começa com um carimbo de tempo em milissegundos e termina com bits aleatórios. O resultado é ordenável por data de criação, o que ajuda muito o índice do banco.
  • UUIDv1: baseado em tempo e no endereço de rede da máquina. Caiu em desuso porque podia vazar o endereço MAC.

A grande novidade da RFC 9562 foi justamente o UUIDv7. Ele resolve o principal problema de performance do v4 sem voltar a expor a ordem real de criação de forma legível para um atacante casual.

Além disso, por ter 128 bits, o UUID é perfeito para sistemas distribuídos: cada serviço gera o seu identificador sem coordenação central e sem risco de colisão.

Como começar: usando UUID no seu banco

A boa notícia é que os bancos modernos já têm suporte direto. No PostgreSQL, existe o tipo nativo uuid e a função gen_random_uuid() para gerar valores v4 sem instalar nada.

Um passo a passo simples:

  • Passo 1: troque a coluna de chave primária de serial ou bigint para o tipo uuid.
  • Passo 2: defina um valor padrão com DEFAULT gen_random_uuid() ou gere o UUID na aplicação.
  • Passo 3: garanta que a sua linguagem use uma biblioteca confiável de UUID (a maioria das stacks tem suporte nativo).

No MySQL e no SQL Server também dá para usar, geralmente armazenando o valor em formato binário de 16 bytes para economizar espaço. Para UUIDv7, hoje você normalmente gera o valor na aplicação, já que nem todo banco traz uma função pronta.

Exemplo prático: de ID sequencial para UUIDv7

Imagine uma tabela de pedidos com ID sequencial. Um concorrente faz um pedido de teste na segunda e outro na sexta. Se os IDs forem 10250 e 10410, ele sabe que você processou cerca de 160 pedidos na semana. Isso é informação estratégica saindo de graça.

Com UUID, o mesmo pedido teria um identificador como 018f1a2b-7c3d-7e4f-8a9b-0c1d2e3f4a5b. Não dá para inferir volume nem ordem a olho nu. No PostgreSQL, a criação da tabela ficaria assim:

CREATE TABLE pedidos (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), valor numeric, criado_em timestamptz DEFAULT now());

Se você escolher UUIDv7 gerado na aplicação, ganha um bónus: como ele começa pelo tempo, os novos registros são inseridos quase sempre no fim do índice, evitando a fragmentação que o v4 puro causa.

Comparação com alternativas

O UUID não é a única saída. Vale conhecer os concorrentes:

  • ULID: 128 bits, ordenável por tempo e escrito em base32, o que gera textos mais curtos e legíveis. Conceito muito parecido com o UUIDv7.
  • Snowflake ID: criado pelo Twitter, usa 64 bits com tempo, ID de máquina e sequência. Cabe em um inteiro, mas exige coordenação de IDs de máquina.
  • Nano ID: foco em URLs curtas, com tamanho configurável. Ótimo para links públicos, menos comum como chave primária.

A regra mental é simples: se você quer suporte nativo no banco e padrão oficial, use UUID (de preferência v7). Se precisa de IDs menores em 64 bits e já tem infraestrutura para isso, o Snowflake pode valer. Se quer algo curto e bonito na URL, o Nano ID resolve.

Para a maioria dos sistemas brasileiros de pequeno e médio porte, o UUID continua sendo a escolha mais segura e com menos surpresas.

Pontos positivos e limitações

Os ganhos são claros: identificadores que não vazam dados de negócio, geração distribuída sem coordenação, e facilidade para juntar dados de vários bancos sem conflito de chaves.

Mas existem custos reais. Um UUID ocupa 16 bytes, contra 4 ou 8 bytes de um inteiro. Em tabelas com bilhões de linhas e muitas chaves estrangeiras, isso pesa no armazenamento e na memória do índice.

O outro ponto é a performance de escrita do UUIDv4: por ser aleatório, ele espalha as inserções pelo índice e causa divisão de páginas. Esse é exatamente o problema que o UUIDv7 veio resolver, então prefira v7 quando o volume for alto.

Casos de uso reais

Onde o UUID brilha de verdade:

  • APIs públicas: quando o ID aparece na URL ou na resposta, o UUID evita que terceiros mapeiem o seu crescimento.
  • Sistemas multi-tenant e SaaS: cada cliente gera registros sem risco de colisão entre bases diferentes.
  • Apps offline-first e mobile: o aparelho cria o ID localmente e sincroniza depois, sem esperar o servidor.
  • Integrações e migrações: juntar dados de vários sistemas fica trivial quando as chaves já são globalmente únicas.

Por outro lado, uma tabela interna de log que ninguém vê de fora e que cresce muito talvez fique melhor com um inteiro sequencial, mais leve. A decisão é por contexto, não por moda.

O ponto de equilíbrio comum é usar UUID na chave pública exposta e manter, se quiser, um inteiro interno só para uso do banco.

Dicas e boas práticas

Quem já usa UUID em produção costuma seguir alguns padrões:

  • Prefira UUIDv7 para novas chaves primárias com alto volume de escrita, pela ordenação temporal.
  • No banco, armazene como tipo uuid nativo (ou binário de 16 bytes), nunca como texto de 36 caracteres, que desperdiça espaço.
  • Não exponha o inteiro sequencial interno em URL ou API ao mesmo tempo, senão o vazamento volta.
  • Valide o formato do UUID na entrada para evitar buscas inválidas.

Um erro clássico de iniciante é guardar UUID como VARCHAR(36). Além de ocupar mais que o dobro do necessário, deixa os índices maiores e mais lentos.

Outro cuidado: lembrar que o UUID protege contra adivinhação, mas não substitui autorização. Continue verificando se o usuário pode acessar aquele recurso, mesmo que o ID seja impossível de adivinhar.

Vale a pena migrar para UUID?

Para quem expõe IDs em URLs, APIs ou documentos públicos, a resposta é quase sempre sim. O custo de alguns bytes a mais é pequeno perto do risco de vazar volume de negócio e abrir a porta para enumeração.

Se o seu projeto é novo, comece já com UUIDv7 nas chaves públicas e evite a dor de uma migração futura. Se o sistema é antigo, dá para migrar aos poucos, expondo um UUID novo sem apagar o inteiro interno de imediato.

O próximo passo é simples: olhe agora para as suas URLs e respostas de API. Se você vê números sequenciais por lá, o German Tank Problem já está trabalhando contra você, e o UUID é a correção mais direta.