Em um projeto legado, encontrei uma funcionalidade aparentemente simples: combinar vários PDFs em um único arquivo.
Na prática, ela era um ponto recorrente de lentidão, falhas silenciosas e consumo excessivo de memória. O fluxo antigo seguia uma lógica comum em sistemas que crescem com o tempo:
- Recebia uma lista de arquivos.
- Tentava combinar tudo diretamente.
- Só descobria problemas durante o processamento.
- Em caso de falha, o usuário recebia uma mensagem genérica.
- Arquivos grandes causavam lentidão e, em alguns casos, estouro de memória.
O problema não estava apenas na biblioteca usada para unir PDFs. O problema estava no fluxo inteiro.
Antes de pensar em trocar ferramenta, refatorei o processo em etapas menores:
Pré-validação dos arquivos
Antes de iniciar o merge, passei a validar:
- se o arquivo existia;
- se era realmente um PDF;
- se estava acessível;
- se estava corrompido;
- se havia pendências impeditivas, como assinatura digital;
- se o arquivo base não estava sendo incluído novamente por engano.
Isso evitou gastar processamento com arquivos inválidos e melhorou muito a clareza dos erros.
Processamento em streaming
O fluxo antigo carregava arquivos inteiros em memória em alguns pontos.
Ajustei o processo para trabalhar com leitura e escrita em fluxo sempre que possível, reduzindo o impacto de arquivos grandes.
Isso trouxe dois ganhos importantes:
- menor consumo de memória;
- maior estabilidade em merges com muitos documentos.
Separação de responsabilidades
O job que fazia tudo foi quebrado em etapas mais claras:
- preparação dos arquivos;
- validação;
- tentativa de merge via serviço externo;
- fallback local;
- paginação;
- persistência do resultado;
- emissão de evento para o frontend.
Com isso, ficou mais simples testar cada parte isoladamente.
Fallback controlado
Em vez de simplesmente falhar quando o serviço externo apresentava erro, o sistema passou a ter um fallback local usando FPDI. Mas com uma diferença importante: o fallback também passou a ter validação, logs e tratamento explícito de erro.
Fallback sem observabilidade só troca um problema por outro.
Resultado
Com essas mudanças, em um cenário anonimizado de teste, o tempo médio de merge caiu aproximadamente 80%.
Além disso, o processo ficou mais previsível:
- menos falhas silenciosas;
- menor consumo de memória;
- mensagens de erro mais úteis;
- fluxo mais testável;
- melhor experiência para o usuário.
A principal lição aqui foi: performance nem sempre melhora trocando biblioteca. Muitas vezes, melhora quando o fluxo deixa de desperdiçar processamento com coisa que já poderia ter sido validada antes. Em sistemas legados, otimização boa não é só “deixar mais rápido”. É deixar mais rápido, mais seguro e mais fácil de manter.

