Quando você está administrando uma corretora de carros no Azerbaijão e importando veículos de salvados dos Estados Unidos, seus problemas de software parecem diferentes dos de uma startup do Vale do Silício. Você não está otimizando para um milhão de usuários simultâneos. Você está otimizando para clareza, uptime e a capacidade de consertar as coisas você mesmo à meia-noite enquanto coordena com uma casa de leilões em um fuso horário de doze horas de diferença. Essa é exatamente a situação em que me encontrei quando construí o AutoMakler. A plataforma gerencia tudo, desde o scraping de leilões ao vivo e consultas ao Carfax até estimativas de entrega e processamento de pagamentos. É um sistema de produção real atendendo clientes reais, e ele roda no que a maioria dos desenvolvedores chamaria de uma stack agressivamente entediante.

A Stack que Ninguém Quer Apresentar

Não tem React. Nem Vue. Nem Redis, nem Celery, nem um servidor WebSocket. O backend é FastAPI com Python puro. O banco de dados é PostgreSQL. O frontend é HTML renderizado no servidor usando templates Jinja2, Bootstrap e uma pitada de vanilla JavaScript. Para scraping, eu uso Playwright. Tudo roda como um único processo Python que serve HTML diretamente.

Não há etapa de build. Não há pastas node_modules para auditar, nem transpiladores para configurar, nem a rotatividade de frameworks de frontend para acompanhar. Quando faço o deploy, estou movendo arquivos Python e templates, não orquestrando um pipeline de bundlers. Essa simplicidade não é uma concessão. É o objetivo principal.

Como Enfileirar Tarefas Sem um Broker de Mensagens

Fazer o scraping de um leilão de carros ao vivo não pode acontecer de forma síncrona. Um único scraping pode levar vários segundos enquanto o Playwright carrega a página, executa JavaScript e extrai os dados. Bloquear o usuário enquanto isso acontece não é uma opção. O manual padrão diz para instalar o Redis, configurar o Celery e subir um pool de workers. Eu pulei tudo isso.

Em vez disso, o AutoMakler usa o Postgres como seu próprio enfileirador de tarefas. Quando um usuário aciona um scraping, a aplicação escreve uma nova linha em uma tabela de tarefas com o status pending. Uma tarefa de background asyncio pega essa linha e inicia o scraping no navegador. Enquanto isso, o navegador faz o polling de um endpoint leve a cada três segundos para verificar o status. Quando a linha é atualizada para completed, a página atualiza e exibe os resultados.

Esse padrão funciona porque o intervalo de polling é curto o suficiente para parecer responsivo, mas longo o suficiente para evitar sobrecarregar o servidor. Três segundos é uma eternidade para um computador e quase imperceptível para um humano esperando por um site de leilão externo. O banco de dados lida com a concorrência nativamente e, como as tarefas são apenas linhas no Postgres, posso inspecionar a fila com uma simples consulta SQL em vez de vasculhar logs do Celery ou chaves do Redis.

Mantendo o Servidor Vivo Sem um Pool de Workers

A automação de navegador consome muita memória. Inicie muitas instâncias do Playwright de uma vez e seu servidor irá colapsar. A correção convencional é um pool de workers gerenciado com limites de concorrência, muitas vezes apoiado pela mesma combinação de Redis e Celery. Eu uso uma linha de Python: um asyncio.Semaphore.

O semáforo limita quantas instâncias simultâneas do navegador podem rodar. Quando uma nova solicitação de scraping chega, ela ou ocupa uma vaga imediatamente ou espera até que uma seja liberada. Tudo isso acontece dentro do mesmo processo. Não há um orquestrador externo para falhar, nenhum processo worker para morrer silenciosamente e nenhuma infraestrutura adicional para monitorar. Minha memória permanece previsível, e o código que protege o servidor está logo ao lado do código que o utiliza, não escondido em um manifesto de deploy.

Roteando Dinheiro com Uma Única URL de Callback

O processamento de pagamentos introduziu uma restrição que eu não pude alterar. Meu gateway de pagamento permite exatamente uma URL de callback por conta de comerciante, mas eu precisava processar transações para dois projetos separados através dessa única conta. Criar um segundo perfil de comerciante significaria taxas extras, conformidade extra e burocracia extra para a qual uma pequena corretora não tem tempo.

A solução foi codificar o nome do projeto diretamente na string do ID do pedido antes de enviar o cliente para o gateway. Quando o callback atinge meu servidor, o AutoMakler decodifica esse ID, identifica a qual projeto o pagamento pertence e roteia a notificação para o manipulador interno correto. A lógica existente permaneceu intacta. Isso é design aditivo: eu não reescrevi o fluxo de pagamento, apenas fiz com que o identificador carregasse um pouco mais de contexto. É o tipo de hack que parece óbvio em retrospectiva, mas economiza horas de ginástica arquitetural.

Chat que Funciona Sem WebSockets

Chats de suporte ao cliente são geralmente onde os engenheiros cedem e adicionam WebSockets. Eu precisava de mensagens dentro do aplicativo, mas também precisava manter a pegada de infraestrutura mínima. Então, reutilizei a mesma estratégia de polling que alimenta as raspagens de leilão.

As mensagens são armazenadas no Postgres. Quando um usuário envia uma mensagem, ela é gravada na tabela. O cliente faz polling em busca de atualizações, e a UI reflete novas mensagens e confirmações de leitura quase em tempo real. Para manter isso rápido mesmo conforme a tabela de conversas cresce, adicionei um índice parcial no Postgres que cobre apenas mensagens não lidas de conversas ativas. O banco de dados não desperdiça ciclos escaneando o histórico antigo, e o query planner consegue satisfazer a maioria das buscas de chat com um escaneamento de intervalo de índice restrito.

Para um chat de suporte onde alguns segundos de latência são aceitáveis, isso é perfeitamente adequado. Os usuários recebem o feedback de que precisam, e eu nunca precisei depurar uma conexão WebSocket obsoleta ou gerenciar um servidor de socket separado.

As Desvantagens Reais

Esta arquitetura envolve trade-offs reais, e fingir o contrário seria desonesto. O polling gera muito tráfego. A cada três segundos, cada cliente ativo acessa o servidor. A largura de banda e a carga de consultas são maiores do que uma conexão de socket persistente exigiria. Se o processo Python reiniciar, qualquer tarefa de segundo plano em execução morre imediatamente porque não há um worker externo para retomá-la. Eu aceito isso porque as tarefas são pequenas e o custo de uma nova tentativa é baixo. Uma raspagem de navegador que falhe pode simplesmente ser reiniciada pelo usuário.

Também há um limite para essa abordagem. Se o AutoMakler algum dia precisar atender milhares de raspagens simultâneas, o modelo de processo único com polling ficará sobrecarregado. Mas esse não é o negócio em que estou. Eu preciso de confiabilidade para dezenas de usuários simultâneos, não