Numeração de faturas segura contra concorrência que evita duplicatas e lacunas
Aprenda padrões práticos para numeração de faturas segura contra concorrência, permitindo que vários usuários criem faturas ou tickets sem duplicatas ou lacunas inesperadas.

O que dá errado quando duas pessoas criam registros ao mesmo tempo
Imagine um escritório movimentado às 16:55. Duas pessoas terminam uma fatura e clicam em Salvar com um segundo de diferença. Ambas as telas mostram rapidamente “Fatura #1042”. Um registro vence, o outro falha, ou pior, ambos são armazenados com o mesmo número. Esse é o sintoma mais comum no mundo real: números duplicados que aparecem só sob carga.
Tickets se comportam da mesma forma. Dois atendentes criam um novo ticket para o mesmo cliente ao mesmo tempo, e seu sistema tenta “pegar o próximo número” olhando o registro mais recente. Se as duas requisições lerem o mesmo valor “mais recente” antes de qualquer uma escrever, ambas podem escolher o mesmo próximo número.
O segundo sintoma é mais sutil: números pulados. Você pode ver #1042, depois #1044, com #1043 faltando. Isso costuma acontecer após um erro ou tentativa de retry. Uma requisição reserva um número e depois o salvamento falha por um erro de validação, timeout ou o usuário fechar a aba. Ou um job em background tenta novamente após um problema de rede e pega um número novo mesmo que a primeira tentativa já tenha consumido um.
Para faturas isso importa porque a numeração faz parte do seu trilho de auditoria. Contadores esperam que cada fatura seja identificada unicamente, e clientes podem referenciar números de fatura em pagamentos ou e-mails de suporte. Para tickets, o número é o identificador que todos usam em conversas, relatórios e exports. Duplicatas geram confusão. Números faltando podem levantar perguntas durante revisões, mesmo quando nada desonesto ocorreu.
Aqui está a expectativa chave para definir cedo: nem todo método de numeração consegue ser ao mesmo tempo seguro contra concorrência e sem lacunas. A numeração segura contra concorrência (sem duplicatas, mesmo com muitos usuários) é alcançável e não deve ser negociável. Numeração sem lacunas também é possível, mas exige regras extras e frequentemente muda como você trata rascunhos, falhas e cancelamentos.
Uma boa forma de enquadrar o problema é perguntar o que você precisa que seus números garantam:
- Nunca devem se repetir (únicos, sempre)
- Devem ser majoritariamente crescentes (bom ter)
- Nunca devem pular (apenas se você projetar assim)
Uma vez que você escolha a regra, a solução técnica fica muito mais fácil de escolher.
Por que duplicatas e lacunas acontecem
A maioria dos apps segue um padrão simples: o usuário clica em Salvar, o app pede o próximo número de fatura ou ticket, e então insere o novo registro com esse número. Parece seguro porque funciona perfeitamente quando só uma pessoa está fazendo isso.
O problema começa quando dois salvamentos acontecem quase ao mesmo tempo. Ambas as requisições podem chegar no passo “pegar o próximo número” antes de qualquer uma terminar o insert. Se as duas leituras virem o mesmo valor “próximo”, as duas tentam gravar o mesmo número. Isso é uma condição de corrida: o resultado depende do tempo, não da lógica.
Uma linha do tempo típica é assim:
- Requisição A lê próximo número: 1042
- Requisição B lê próximo número: 1042
- Requisição A insere fatura 1042
- Requisição B insere fatura 1042 (ou falha se uma regra de unicidade bloquear)
Duplicatas acontecem quando nada no banco impede o segundo insert. Se você só checa “esse número já existe?” no código da aplicação, ainda pode perder a corrida entre a checagem e o insert.
Lacunas são um problema diferente. Elas ocorrem quando seu sistema “reserva” um número, mas o registro nunca se torna uma fatura ou ticket real e confirmado. Causas comuns são pagamentos falhados, erros de validação detectados tarde, timeouts ou um usuário fechando a aba depois do número ser atribuído. Mesmo se o insert falhar e nada for salvo, o número pode já ter sido consumido.
Concorrência oculta torna isso pior porque raramente é “apenas duas pessoas clicando Salvar.” Você também pode ter:
- Clientes de API criando registros em paralelo
- Imports que rodam em batches
- Jobs em background gerando faturas durante a noite
- Retries de apps móveis com conexões instáveis
Então as causas principais são: (1) conflitos de tempo quando múltiplas requisições leem o mesmo valor do contador, e (2) números sendo alocados antes de você ter certeza que a transação irá ter sucesso. Qualquer plano para numeração segura contra concorrência precisa decidir qual resultado você tolera: sem duplicatas, sem lacunas, ou ambos, e exatamente em quais eventos (rascunhos, retries, cancelamentos).
Decida sua regra de numeração antes de escolher uma solução
Antes de projetar a numeração segura contra concorrência, escreva o que o número deve significar no seu negócio. O erro mais comum é escolher um método técnico primeiro e depois descobrir que regras contábeis ou legais esperam algo diferente.
Comece separando dois objetivos que costumam se misturar:
- Único: duas faturas ou tickets nunca compartilham o mesmo número.
- Sem lacunas: números são únicos e também estritamente consecutivos (sem faltas).
Muitos sistemas reais miram em apenas único e aceitam lacunas. Lacunas podem acontecer por razões normais: um usuário abre um rascunho e o abandona, um pagamento falha depois que o número é reservado, ou um registro é criado e depois anulado. Para tickets, lacunas geralmente não importam. Mesmo para faturas, muitas equipes aceitam lacunas se puderem explicá-las com um trilho de auditoria (anulado, cancelado, teste, etc.). Numeração sem lacunas é possível, mas força regras extras e frequentemente adiciona atrito.
Depois, decida o escopo do contador. Pequenas diferenças no enunciado mudam muito o design:
- Uma sequência global para tudo, ou sequências separadas por empresa/tenant?
- Resetar a cada ano (2026-000123) ou nunca resetar?
- Séries diferentes para faturas vs notas de crédito vs tickets?
- Precisa de um formato amigável ao humano (prefixos, separadores), ou só um número interno?
Um exemplo concreto: um produto SaaS com múltiplas empresas clientes pode exigir números de fatura que sejam únicos por empresa e resetem por ano, enquanto tickets são únicos globalmente e nunca resetam. São dois contadores diferentes com regras distintas, mesmo que a UI pareça similar.
Se você realmente precisa de sem lacunas, seja explícito sobre quais eventos são permitidos depois que um número é atribuído. Por exemplo, uma fatura pode ser deletada, ou somente cancelada? Usuários podem salvar rascunhos sem número e atribuir o número somente na aprovação final? Essas escolhas frequentemente importam mais que a técnica de banco de dados.
Escreva a regra em uma especificação curta antes de construir:
- Quais tipos de registro usam a sequência?
- O que torna um número “usado” (rascunho, enviado, pago)?
- Qual é o escopo (global, por empresa, por ano, por série)?
- Como tratar anulações e correções?
No AppMaster, esse tipo de regra pertence próximo ao seu modelo de dados e fluxo de processo de negócio, assim a equipe implementa o mesmo comportamento em API, web UI e mobile sem surpresas.
Abordagens comuns e o que cada uma garante
Quando as pessoas falam sobre “numeração de faturas”, elas misturam dois objetivos: (1) nunca gerar o mesmo número duas vezes, e (2) nunca ter lacunas. A maioria dos sistemas pode garantir facilmente o primeiro. O segundo é bem mais difícil, porque lacunas podem aparecer sempre que uma transação falha, um rascunho é abandonado ou um registro é anulado.
Abordagem 1: Sequence do banco (unicidade rápida)
Uma sequence do PostgreSQL é a maneira mais simples de obter números únicos e crescentes sob carga. Ela escala bem porque o banco é feito para entregar valores de sequence rapidamente, mesmo com muitos usuários criando registros ao mesmo tempo.
O que você ganha: unicidade e ordenação (maiormente crescente). O que você não ganha: números sem lacunas. Se um insert falha depois que um número foi atribuído, esse número fica “gasto” e você verá uma lacuna.
Abordagem 2: Constraint única + retry (deixe o banco decidir)
Aqui você gera um número candidato (pela lógica da aplicação), salva e conta com uma constraint UNIQUE para rejeitar duplicatas. Se houver conflito, você tenta novamente com um número novo.
Isso pode funcionar, mas tende a ficar barulhento sob alta concorrência. Você pode ter muitos retries, transações falhadas e picos mais difíceis de depurar. Também não garante números sem lacunas, a menos que seja combinado com regras rígidas de reserva, o que adiciona complexidade.
Abordagem 3: Linha de contador com lock (visando sem lacunas)
Se você realmente precisa de números sem lacunas, o padrão usual é uma tabela dedicada de contadores (uma linha por escopo de numeração, como por empresa ou por ano). Você faz lock nessa linha dentro de uma transação, incrementa e usa o novo valor.
Isso é o mais próximo de sem lacunas em design tradicional de banco, mas tem custo: cria um ponto “quente” que todos os gravadores precisam aguardar. Também aumenta o risco operacional (transações longas, timeouts e deadlocks).
Abordagem 4: Serviço de reserva separado (apenas para casos especiais)
Um “serviço de numeração” separado pode centralizar regras entre vários apps ou bancos. Geralmente vale a pena quando você tem vários sistemas emitindo números e não pode consolidar as escritas.
A troca é risco operacional: você adicionou outro serviço que deve ser correto, altamente disponível e consistente.
Aqui está uma forma prática de pensar sobre garantias para numeração segura contra concorrência:
- Sequence: único, rápido, aceita lacunas
- Único + retry: único, simples em baixa carga, pode triturar sob alta carga
- Linha de contador com lock: pode ser sem lacunas, mais lento sob concorrência pesada
- Serviço separado: flexível entre sistemas, maior complexidade e modos de falha
Se você está construindo isso em uma ferramenta no-code como o AppMaster, as mesmas escolhas se aplicam: o banco é onde a correção vive. A lógica do app pode ajudar com retries e mensagens de erro claras, mas a garantia final deve vir de constraints e transações.
Passo a passo: prevenir duplicatas com sequences e constraints únicas
Se seu objetivo principal é evitar duplicatas (não garantir ausência de lacunas), o padrão mais simples e confiável é: deixe o banco gerar um ID interno, e aplique unicidade no número mostrado ao cliente.
Comece separando os dois conceitos. Use um valor gerado pelo banco (identity/sequence) como chave primária para joins, edições e exports. Mantenha invoice_no ou ticket_no como uma coluna separada exibida para as pessoas.
Uma configuração prática em PostgreSQL
Aqui está uma abordagem comum em PostgreSQL que mantém a lógica do “próximo número” dentro do banco, onde a concorrência é tratada corretamente.
-- Internal, never-shown primary key
create table invoices (
id bigint generated always as identity primary key,
invoice_no text not null,
created_at timestamptz not null default now()
);
-- Business-facing uniqueness guarantee
create unique index invoices_invoice_no_uniq on invoices (invoice_no);
-- Sequence for the visible number
create sequence invoice_no_seq;
Agora gere o número de exibição no momento do insert (não fazendo "select max(invoice_no) + 1"). Um padrão simples é formatar um valor de sequence dentro do INSERT:
insert into invoices (invoice_no)
values (
'INV-' || lpad(nextval('invoice_no_seq')::text, 8, '0')
)
returning id, invoice_no;
Mesmo se 50 usuários clicarem “Criar fatura” ao mesmo tempo, cada insert obtém um valor de sequence diferente e o índice único impede duplicatas acidentais.
O que fazer quando houver colisão
Com uma sequence simples, colisões são raras. Geralmente acontecem quando você adiciona regras extras como “reset por ano”, “por tenant” ou números editáveis pelo usuário. Por isso a constraint UNIQUE ainda é importante.
No nível da aplicação, trate o erro de violação de unicidade com um pequeno loop de retry. Mantenha-o simples e com limite:
- Tente o insert
- Se receber um erro de constraint única em invoice_no, tente novamente
- Pare após um pequeno número de tentativas e mostre um erro claro
Isso funciona bem porque retries são disparados apenas quando algo incomum acontece, como dois caminhos de código produzindo o mesmo número formatado.
Mantenha a janela de corrida pequena
Não calcule o número na UI e não “reserve” números lendo primeiro e inserindo depois. Gere-o o mais próximo possível da escrita no banco.
Se você usa AppMaster com PostgreSQL, modele o id como chave primária identity no Data Designer, adicione uma constraint única para invoice_no e gere invoice_no durante o fluxo de criação para que aconteça junto com o insert. Assim o banco permanece a fonte da verdade e problemas de concorrência ficam contidos onde o PostgreSQL é mais forte.
Passo a passo: construir um contador sem lacunas com bloqueio de linha
Se você realmente precisa de números sem lacunas (nenhuma fatura ou ticket faltando), você pode usar uma tabela de contadores transacional e bloqueio de linha. A ideia é simples: apenas uma transação por vez pode pegar o próximo número para um dado escopo, então os números são entregues em ordem.
Primeiro, decida seu escopo. Muitas equipes precisam de sequências separadas por empresa, por ano ou por série (como INV vs CRN). A tabela de contadores armazena o último número usado para cada escopo.
Aqui está um padrão prático para numeração segura usando bloqueios de linha no PostgreSQL:
- Crie uma tabela, por exemplo
number_counters, com colunas comocompany_id,year,series,last_numbere uma chave única em(company_id, year, series). - Inicie uma transação de banco.
- Faça lock na linha do contador para seu escopo usando
SELECT last_number FROM number_counters WHERE ... FOR UPDATE. - Calcule
next_number = last_number + 1, atualize a linha do contador paralast_number = next_number. - Insira a fatura ou ticket usando
next_number, então commit.
A chave é FOR UPDATE. Sob carga, você não terá duplicatas. Também não terá lacunas por causa de “duas pessoas obtiveram o mesmo número”, porque a segunda transação não pode ler e incrementar a mesma linha de contador até a primeira commitar (ou dar rollback). Em vez disso, a segunda requisição espera brevemente. Essa espera é o preço por ser sem lacunas.
Inicializando um novo escopo
Você também precisa de um plano para a primeira vez que um escopo aparecer (nova empresa, novo ano, nova série). Duas opções comuns:
- Pré-criar linhas de contador antecipadamente (por exemplo, criar as linhas do próximo ano em dezembro).
- Criar sob demanda: tente inserir a linha do contador com
last_number = 0, e se ela já existir, volte ao fluxo normal de lock e incremento.
Se você construir isso em uma ferramenta no-code como o AppMaster, mantenha todo o fluxo “lock, incrementar, inserir” dentro de uma única transação na sua lógica de negócio, assim tudo acontece ou nada acontece.
Casos de borda: rascunhos, salvamentos falhos, cancelamentos e edições
A maioria dos bugs de numeração aparece nas partes confusas: rascunhos que nunca são postados, salvamentos que falham, faturas que são anuladas e registros que são editados depois que alguém já viu o número. Se você quer numeração segura contra concorrência, precisa de uma regra clara sobre quando o número se torna “real”.
A maior decisão é o timing. Se você atribui um número no momento em que alguém clica “Nova fatura”, você terá lacunas por rascunhos abandonados. Se você atribui apenas quando uma fatura é finalizada (postada, emitida, enviada ou o que quer que signifique “final” no seu negócio), você consegue manter os números mais contidos e fáceis de explicar.
Salvamentos falhos e rollbacks são onde expectativas frequentemente batem com o comportamento do banco. Com uma sequence típica, uma vez que um número é tomado ele está tomado, mesmo se a transação falhar depois. Isso é normal e seguro, mas pode criar lacunas. Se sua política exige números sem lacunas, o número deve ser atribuído somente no passo final e somente se a transação commitar. Isso geralmente significa bloquear uma única linha de contador, escrever o número final e commitar em uma única unidade. Se qualquer passo falhar, nada é atribuído.
Cancelamentos e anulações quase nunca devem “reusar” um número. Mantenha o número e mude o status. Auditores e clientes esperam que o histórico se mantenha consistente, mesmo quando um documento é corrigido.
Edições são mais simples: uma vez que um número é visível fora do sistema, trate-o como permanente. Nunca renumere uma fatura ou ticket depois que ele foi compartilhado, exportado ou impresso. Se precisar corrigir, crie um novo documento e referencie o antigo (por exemplo, uma nota de crédito ou um ticket de substituição), mas não reescreva o histórico.
Um conjunto prático de regras que muitas equipes adotam:
- Rascunhos não têm número final (use um ID interno ou “RASCUNHO”).
- Atribua o número apenas no passo “Post/Emitir”, dentro da mesma transação da mudança de status.
- Anulações e cancelamentos mantêm o número, mas recebem status e motivo claros.
- Números impressos/enviados por e-mail nunca mudam.
- Imports preservam números originais e atualizam o contador para o próximo valor seguro.
Migrações e imports merecem cuidado especial. Se você migrar de outro sistema, traga os números de fatura existentes como estão e então ajuste seu contador para começar após o valor máximo importado. Também decida o que fazer com formatos conflitantes (como prefixos diferentes por ano). Normalmente é melhor armazenar o “número exibido” exatamente como era e manter uma chave primária interna separada.
Exemplo: uma helpdesk cria tickets rapidamente, mas muitos são rascunhos. Atribua o número do ticket apenas quando o agente clicar “Enviar ao cliente”. Isso evita gastar números com rascunhos abandonados e mantém a sequência visível alinhada com a comunicação real ao cliente. Em uma ferramenta no-code como o AppMaster, a mesma ideia se aplica: mantenha rascunhos sem número público e gere o número final durante o passo de “submeter” no Business Process que commita com sucesso.
Erros comuns que causam duplicatas ou lacunas inesperadas
A maioria dos problemas de numeração vem de uma ideia simples: tratar um número como um valor de exibição em vez de um estado compartilhado. Quando várias pessoas salvam ao mesmo tempo, o sistema precisa de um lugar claro para decidir o próximo número e uma regra clara sobre o que acontece em caso de falha.
Um erro clássico é usar SELECT MAX(number) + 1 no código da aplicação. Parece certo em testes de um usuário, mas duas requisições podem ler o mesmo MAX antes de qualquer uma commitar. Ambas geram o mesmo próximo valor e você tem uma duplicata. Mesmo se adicionar um “checar e tentar de novo”, você ainda pode criar carga extra e picos estranhos em horários de pico.
Outra fonte comum de duplicatas é gerar o número no lado do cliente (browser ou mobile) antes de salvar. O cliente não sabe o que outros usuários estão fazendo e não pode reservar um número com segurança se o salvamento falhar. Números gerados no cliente são aceitáveis para rótulos temporários como “Rascunho 12”, mas não para IDs oficiais de fatura ou ticket.
Lacunas surpreendem equipes que assumem que sequences são sem lacunas. No PostgreSQL, sequences são projetadas para unicidade, não para continuidade perfeita. Números podem ser pulados quando uma transação dá rollback, quando você prefetch de IDs ou quando o banco reinicia. Isso é comportamento normal. Se seu requisito real é “não ter duplicatas”, uma sequence + constraint única geralmente é a resposta certa. Se seu requisito é realmente “sem lacunas”, você precisa de outro padrão (geralmente locking de linha) e aceitar algumas trocas em throughput.
Locking também pode dar errado quando é amplo demais. Um lock global único para toda numeração força cada criação em uma fila, mesmo quando você poderia particionar contadores por empresa, local ou tipo de documento. Isso pode desacelerar o sistema e fazer usuários sentirem que o salvamento está “aleatoriamente” travado.
Aqui estão os erros a checar ao implementar numeração segura:
- Usar
MAX + 1(ou “encontrar o último número”) sem uma constraint de unicidade em nível de banco. - Gerar números finais no cliente e tentar “consertar conflitos depois”.
- Esperar que sequences do PostgreSQL sejam sem lacunas e tratar lacunas como erros.
- Fazer lock de um contador compartilhado para tudo, em vez de particionar onde faz sentido.
- Testar só com um usuário, de modo que condições de corrida só apareçam após o lançamento.
Dica prática de teste: rode um teste de concorrência simples que crie de 100 a 1.000 registros em paralelo e depois verifique duplicatas e lacunas inesperadas. Se você construir em uma ferramenta no-code como o AppMaster, a mesma regra se aplica: assegure que o número final seja atribuído dentro de uma única transação server-side, não no fluxo da UI.
Verificações rápidas antes de enviar para produção
Antes de liberar a numeração de faturas ou tickets, faça uma checagem rápida nas partes que costumam falhar sob tráfego real. O objetivo é simples: cada registro recebe exatamente um número de negócio, e suas regras permanecem verdadeiras mesmo quando 50 pessoas clicam “Criar” ao mesmo tempo.
Aqui está uma checklist prática pré-lançamento para numeração segura:
- Confirme que o campo do número de negócio tem constraint única no banco (não só checagem na UI). Essa é sua última linha de defesa se duas requisições colidirem.
- Garanta que o número é atribuído dentro da mesma transação de banco que salva o registro. Se atribuição do número e salvamento estiverem separados entre requisições, você verá duplicatas eventualmente.
- Se exigir números sem lacunas, atribua o número apenas quando o registro for finalizado (por exemplo, quando uma fatura é emitida, não quando um rascunho é criado). Rascunhos, formulários abandonados e pagamentos falhos são as fontes mais comuns de lacunas.
- Adicione uma estratégia de retry para conflitos raros. Mesmo com locking de linha ou sequences, você pode encontrar erro de serialização, deadlock ou violação de unicidade em casos de timing extremos. Um retry simples com backoff curto costuma bastar.
- Faça stress test com 20 a 100 criações simultâneas por todos os pontos de entrada: UI, API pública e imports em lote. Teste combinações realistas como rajadas, redes lentas e double submits.
Uma forma rápida de validar sua configuração é simular um momento de helpdesk ocupado: dois agentes abrem o formulário “Novo ticket”, um submete pelo web app enquanto um job de import insere tickets do e-mail ao mesmo tempo. Depois da execução, verifique se todos os números são únicos, têm o formato correto e se falhas não deixaram registros meio salvos.
Se você construir o workflow no AppMaster, os mesmos princípios se aplicam: mantenha a atribuição do número dentro da transação do banco, confie nas constraints do PostgreSQL e teste ações da UI e endpoints de API que criam a mesma entidade. É aí que muitas equipes se sentem seguras em testes manuais, mas se surpreendem no primeiro dia com muitos usuários.
Exemplo: helpdesk ocupado e o que fazer a seguir
Imagine uma central de suporte onde agentes criam tickets o dia todo no web app, enquanto integrações também geram tickets de um chat e de e-mails. Todos esperam números como T-2026-000123 e que cada número aponte para exatamente um ticket.
Uma abordagem ingênua é: ler “último número de ticket”, somar 1 e salvar o novo ticket. Sob carga, duas requisições podem ler o mesmo “último número” antes de qualquer uma salvar. Ambas calculam o mesmo próximo número e você terá duplicatas. Se tentar “consertar” isso com retries após uma falha, frequentemente cria lacunas sem querer.
O banco pode impedir duplicatas mesmo se seu código de app for ingênuo. Adicione uma constraint UNIQUE na coluna ticket_number. Então, quando duas requisições tentarem o mesmo número, um insert falha e você pode tentar novamente com segurança. Isso é o núcleo da numeração segura também para faturas: deixe o banco fazer a garantia de unicidade, não a UI.
Numeração sem lacunas muda o fluxo de trabalho. Se você exigir ausência de lacunas, geralmente não pode atribuir o número final quando o ticket é criado (rascunho). Em vez disso, crie o ticket com status como Draft e sem ticket_number final. Atribua o número apenas quando o ticket for finalizado, assim salvamentos falhos e rascunhos abandonados não “queimam” números.
Um design simples de tabela fica assim:
- tickets: id, created_at, status (Draft, Open, Closed), ticket_number (nullable), finalized_at
- ticket_counters: key (por exemplo "tickets_2026"), next_number
No AppMaster, você pode modelar isso no Data Designer com tipos PostgreSQL e então construir a lógica no Business Process Editor:
- Criar Ticket: inserir ticket com status=Draft e sem ticket_number
- Finalizar Ticket: iniciar uma transação, bloquear a linha do contador, definir ticket_number, incrementar next_number, commitar
- Testar: executar duas ações de “Finalizar” ao mesmo tempo e confirmar que nunca há duplicatas
O que fazer depois: comece definindo sua regra (apenas único vs realmente sem lacunas). Se você puder aceitar lacunas, uma sequence do banco + constraint única costuma ser suficiente e mantém o fluxo simples. Se for obrigatório ser sem lacunas, mova a numeração para o passo de finalização e trate “rascunho” como um estado de primeira classe. Depois faça testes de carga com múltiplos agentes clicando ao mesmo tempo e com integrações de API disparando rajadas, para ver o comportamento antes dos usuários reais.


