15 de maio de 2026

Por que abandonamos UUIDs e como nossos IDs funcionam

Como projetamos os IDs da AbacatePay para serem legíveis, seguros e à prova de confusão — e por que isso importa mais do que parece.

Você abre um chamado de suporte. O cliente colou isso no campo de descrição:

550e8400-e29b-41d4-a716-446655440000

É um cliente? Uma cobrança? Uma assinatura? Uma transação? Você não sabe. Precisa abrir o banco, rodar uma query, torcer pra ter índice no campo certo. Só aí descobre que era uma cobrança — e que o cliente estava se referindo ao checkout errado.

UUIDs são ótimos para bancos de dados. Pra humanos, são ruído.

Na AbacatePay, tomamos uma decisão deliberada desde o início: todos os recursos públicos da API têm IDs que dizem o que são. Um cliente começa com cust_. Uma cobrança começa com bill_. Uma assinatura começa com subs_. Você lê o ID e já sabe o contexto — sem query, sem suposição.

Este post explica como isso funciona, o que motivou cada escolha de design, e por que a matemática por trás do sistema garante que colisões não são um problema real.


O problema com UUIDs

UUID v4 tem 122 bits de entropia. É seguro, praticamente impossível de colidir, e é suportado em tudo. Por que não usá-lo?

O problema não é técnico. É humano.

Um UUID não carrega contexto. Ao olhar para 550e8400-e29b-41d4-a716-446655440000, você não sabe se é um cliente, uma cobrança, ou qualquer outra coisa. Em logs, isso significa cruzar referências manualmente. Em suporte, significa pedir o tipo do recurso além do ID. Em debugging, significa uma etapa extra toda vez.

Existe outro problema mais sutil: hifens. Copiar um UUID numa URL, num terminal, ou num campo de formulário frequentemente dá errado — o hífen quebra seleção de texto duplo-clique, causa problemas em alguns parsers de URL, e torna o ID menos seguro de manipular programaticamente.


Como nossos IDs são estruturados

Todo ID da AbacatePay segue o formato {prefix}_{random}.

O prefixo identifica o tipo do recurso. O sufixo é uma string aleatória de 24 caracteres. Junto, eles formam algo assim:

cust_3FpRmKw2XnBqTsYa4hGdZcEj
bill_mNxPvQ7RtAeKbWsUcFgHdZjY
pix_char_2QwEkMnRxBpFtHcYvNdLsG
webh_prod_KpTnFmXrBqWsYaHdZcEjNv

Você vê o tipo imediatamente. Não precisa de documentação para interpretar um log cheio desses IDs.

Os prefixos cobrem toda a taxonomia da plataforma:

Prefixo Recurso
cust Cliente
bill Cobrança
char Charge (cartão)
pix_char Charge Pix
bole Boleto
subs Assinatura
prod Produto
tran Transferência
wall Carteira
webh_dev / webh_prod Webhook (dev/prod)
kyc KYC
splt Split
affi Afiliado
plug Plugin
app Aplicação

E assim por diante — são mais de 40 prefixos que cobrem cada entidade do sistema.


O alfabeto

Aqui é onde fica interessante.

O sufixo aleatório não usa base64, nem base62, nem hex. Usamos um alfabeto customizado de 52 caracteres, construído para minimizar confusão visual:

abcdefghjkmnpqrstuwxyz   ← minúsculas (22 chars)
0123456                  ← dígitos (7 chars)
ABCDEFGHJKLMNPQRSTUWXYZ  ← maiúsculas (23 chars)

Cada exclusão foi intencional:

O objetivo é o mesmo em todos os casos: se alguém precisar copiar um ID manualmente — de um print de tela, de uma mensagem de voz, de um papel — as chances de erro caem drasticamente.

No código, isso é implementado com uma linha:

import { customAlphabet } from "nanoid";

const alphabet = customAlphabet(
  "abcdefghjkmnpqrstuwxyz0123456ABCDEFGHJKLMNPQRSTUWXYZ",
);

export function createPublicId(prefix: Prefix) {
  return `${prefix}_${alphabet(24)}`;
}

É simples. Não reinventamos geração de aleatório — usamos o nanoid, que é auditado, amplamente adotado e usa crypto.getRandomValues internamente. O que customizamos foi o alfabeto, para refletir nossas necessidades.


A matemática das colisões

Com um alfabeto de 52 caracteres e comprimento 24, o espaço total de IDs possíveis é:

52^24 ≈ 1.53 × 10^41

Para comparar: UUID v4 tem 2^122 ≈ 5.3 × 10^36 combinações. O espaço dos nossos IDs é cerca de 29.000 vezes maior do que o de um UUID. Em bits, isso equivale a 136,8 bits de entropia — contra os 122 bits do UUID.

O nanoid oferece uma fórmula para estimar quando a probabilidade de colisão se torna relevante. Usando o paradoxo do aniversário, a quantidade de IDs que você precisaria gerar para ter 1% de chance de colisão é:

n ≈ √(0.0201 × 52^24) ≈ 5.54 × 10^19

Isso são 55 quintilhões de IDs.

Para ter noção de escala: se a AbacatePay gerasse 1 milhão de IDs por segundo — um ritmo que nenhuma fintech real atinge para identificadores únicos — levaria 1,76 milhão de anos para chegar a 1% de chance de colisão.

Colisão não é um risco operacional. É uma impossibilidade prática.


Como a gente se compara

UUID

É o ponto de partida que a maioria escolhe por inércia. Funciona para banco de dados. Falha em tudo que envolve humanos: logs, suporte, debugging, comunicação entre times.

Stripe

A Stripe foi pioneira nos IDs com prefixo no mundo fintech. Você já viu cus_, ch_, pi_, sub_ — e imediatamente sabe o que é. Esse padrão foi uma das melhores decisões de DX que a Stripe tomou, e nós seguimos a mesma filosofia.

A diferença técnica está no alfabeto. A Stripe usa base62 completa (0-9a-zA-Z — 62 caracteres), sem excluir os caracteres visualmente ambíguos. Isso significa que um ID Stripe pode conter l, I, O, 0 ao mesmo tempo. Para a maioria dos casos isso não importa, mas quando importa — e em fintech, os casos extremos importam — a AbacatePay é mais conservadora.

Polar

O Polar também adota IDs tipados por prefixo, seguindo a mesma escola de pensamento que a Stripe popularizou. A diferença entre plataformas está principalmente nos detalhes de implementação: tamanho do sufixo, alfabeto utilizado e granularidade dos tipos de recurso.

A convergência da indústria para esse modelo é um sinal: IDs com semântica embutida são objetivamente melhores para developer experience do que identificadores opacos.


Por que isso importa em fintech

Em um sistema de pagamentos, um ID errado pode representar uma cobrança errada, uma transferência para o destinatário errado, ou um cancelamento do recurso errado. A superfície de erro importa mais aqui do que em outros domínios.

Quando o ID já diz o que é — cust_ é sempre um cliente, bill_ é sempre uma cobrança — você tem uma camada de validação semântica antes mesmo de fazer qualquer chamada de API. O seu código pode verificar o prefixo antes de enviar uma requisição. O seu time de suporte pode identificar o tipo do recurso sem abrir o banco. Os seus logs ficam legíveis por humanos sem ferramentas adicionais.

É uma decisão pequena no design que reduz a superfície de erro em todos os lugares onde esses IDs aparecem.


O código que gera esses IDs tem menos de 15 linhas. Mas as escolhas por trás dessas 15 linhas — qual alfabeto usar, quais caracteres excluir, qual comprimento garante segurança sem sacrificar usabilidade — levaram tempo para acertar.

Vale o cuidado. Esses IDs vão aparecer em logs, dashboards, webhooks, chamadas de suporte e código de clientes por anos.