Voltar para Escrita

Arquitetando uma plataforma de dados fiscais que processa 64 milhões de empresas por mês

O Momento Fiscal é uma plataforma SaaS que ajuda times tributários e jurídicos a descobrir quem deve o quê. Antes dela, esse trabalho era manual e descentralizado: um analista consultava a Dívida Ativa da União, depois o Serasa, depois o SPC, depois alguns tribunais, copiava números em planilhas e tentava montar o quadro na mão. Não havia um único lugar para rastrear devedores, identificar passivos tributários ou montar uma proposta de recuperação.

A parte difícil não são as telas. É que ser útil significa consolidar dados de 64 milhões de empresas (CNPJs) por mês, a partir de fontes que nunca foram pensadas para serem consultadas juntas.

A forma do sistema

O núcleo é uma aplicação Ruby on Rails: o PostgreSQL guarda os dados da própria aplicação, o MongoDB guarda o conjunto de 64 milhões de empresas, e Redis e Sidekiq fazem o trabalho pesado.

  • Ingestão roda como jobs assíncronos e em lote no Sidekiq, puxando de SERPRO, Receita Federal, SPC, Serasa, tribunais federais e estaduais e da Dívida Ativa.
  • Normalização reconcilia a mesma empresa descrita de cinco formas diferentes em um único registro no MongoDB, chaveado por CNPJ e agrupado geograficamente.
  • Entrega mantém o caminho de leitura rápido: os dados enriquecidos são pré-computados, então a aplicação responde perguntas em vez de calculá-las sob demanda.

Um exemplo concreto: o dump da Receita Federal

A ilustração mais clara do pipeline inteiro é a única fonte que posso mostrar de ponta a ponta: o dump aberto de CNPJs da Receita Federal. Todo mês a Receita publica o cadastro nacional de empresas inteiro como um conjunto de arquivos ZIP. Não há API: só uma listagem de diretório numa URL previsível (/CNPJ/dados_abertos_cnpj/AAAA-MM/), que o job varre com Nokogiri para descobrir os arquivos daquele mês.

Esses arquivos são grandes, então o job não baixa em série. Ele faz um HEAD para ler o tamanho, divide o arquivo em pedaços de 15 MB e os baixa em paralelo com requisições HTTP Range sobre um pool de conexões persistentes (uma thread por pedaço) e depois remonta tudo em disco. Um único arquivo vira dezenas de downloads pequenos e reexecutáveis, em vez de um stream frágil de vários gigabytes.

Aí os dados revidam. Os CSVs são codificados em Latin-1 e o escape de aspas é quebrado de um jeito que um parser padrão não sobrevive: aspas escapadas soltas, campos delimitados por ";" que confundem o próprio caractere de aspas. Então, antes de parsear, o job recodifica cada linha para ISO-8859-1 (descartando bytes inválidos) e reescreve as sequências quebradas para um caractere placeholder (§) que o leitor de CSV consegue tratar como a aspa de verdade. É a parte sem glamour, os 80% da engenharia de dados: o formato é tecnicamente CSV e, na prática, hostil.

Uma empresa está espalhada por quatro arquivos, todos unidos pelo base_cnpj de 8 dígitos: estabelecimentos, empresas, Simples (regime tributário) e sócios. O CNPJ completo é base (8) + ordem (4) + dígitos verificadores (2), formatado como XX.XXX.XXX/XXXX-XX. O job os processa em ordem: a etapa de estabelecimentos cria cada registro (um upsert chaveado pelo CNPJ completo), e as etapas de empresas e Simples enriquecem esses registros já existentes pelo base_cnpj: razão social, natureza jurídica, capital social, porte, regime tributário. Os sócios vão para uma coleção própria.

Cada arquivo é processado em streaming, não carregado de uma vez: o SmarterCSV lê em blocos de 10 a 50 mil linhas, e cada bloco é serializado e escrito no MongoDB num único bulk_write. MongoDB, e não o Postgres que sustenta a aplicação, porque esse conjunto é write-heavy, em lote e tem 64 milhões de registros de profundidade; upserts em massa são exatamente o que ele faz bem.

Projetando para 64 milhões por mês

Nesse volume, as restrições interessantes são operacionais, não algorítmicas:

  • Idempotência. Jobs falham e são reexecutados. Cada escrita é um upsert chaveado pelo CNPJ, então reexecutar um job converge para o mesmo registro em vez de duplicá-lo. E o pipeline é dividido em etapas independentes (estabelecimentos, empresas, Simples, sócios) com flags de skip, então uma execução que morre no meio retoma no arquivo que quebrou, em vez de recomeçar do zero.
  • Backpressure. Fontes externas têm rate limit e caem. Filas no Sidekiq, retentativas com backoff exponencial e isolamento entre fontes evitavam que um provedor lento travasse o resto.
  • Uma camada de alertas em tempo real sobre Redis trazia as mudanças (uma nova dívida, uma mudança de status) com ~99% de disponibilidade e p95 de ~200ms, fazendo a plataforma parecer viva mesmo com a ingestão sendo um lote mensal.

O que eu tirei disso

Processar 64 milhões de registros por mês parece um problema de dados, e em parte é. Mas a maior parte da engenharia foi em tornar o sistema entediante diante de falhas: idempotente, observável e resiliente a fontes se comportando mal. A métrica de que mais me orgulho não é o 64 milhões: é que o caminho de alertas se manteve em 99% enquanto tudo embaixo dele era bagunçado, lento e fora do nosso controle. Essa é a parte da arquitetura que não aparece numa demo, e é a parte que decide se um produto de dados sobrevive ao contato com produção.