Substitua globais mutáveis compartilhadas por estado explícito com segurança
Aprenda a substituir globais mutáveis compartilhadas encontrando singletons ocultos e movendo estado para dependências com escopo de requisição para evitar condições de corrida.

Por que globais mutáveis compartilhadas causam bugs estranhos e aleatórios
“Compartilhado e mutável” soa sofisticado, mas é simples: um valor vive em um lugar só (compartilhado) e seu código pode mudá‑lo (mutável). Quando várias partes da app leem e escrevem esse mesmo valor, o comportamento começa a depender do tempo, não da intenção.
Por isso esses bugs parecem aleatórios. Com um usuário só clicando, a app pode parecer ok. Sob requisições paralelas, jobs em background ou retries automáticos, duas unidades de trabalho podem tocar o mesmo global ao mesmo tempo. Uma requisição atualiza o valor, e outra usa por acidente, mesmo sendo usuários ou tarefas diferentes.
Um exemplo comum é uma variável currentUser armazenada em um módulo, um “cache global” que guarda dados por requisição, ou um cliente singleton que guarda estado (headers, tokens, um tenant selecionado). Esses são exatamente os casos em que você quer substituir globais mutáveis compartilhadas por estado explícito.
Sintomas típicos parecem com:
- Usuários veem dados de outra pessoa ou ficam logados como a pessoa errada
- “Funciona na minha máquina” que só aparece sob carga
- Testes instáveis que passam ou falham dependendo da ordem
- 401/403 aleatórios porque o token errado foi reutilizado
- Jobs em background que pegam a configuração errada
O objetivo não é “não ter coisas compartilhadas”. Bancos de dados e pools de conexão são compartilhados de propósito. O objetivo é: recursos compartilhados permanecem compartilhados, enquanto o estado específico da requisição é passado (ou criado por requisição) para que nada seja reutilizado silenciosamente.
Isso aparece muito em protótipos gerados por IA. FixMyMess muitas vezes encontra singletons ocultos que parecem inofensivos até o tráfego real chegar.
O que conta como global ou singleton em projetos reais
Um “global” é qualquer pedaço de estado que vive fora de uma requisição, job ou ação do usuário, mas que ainda é lido e escrito enquanto a app roda. Um “singleton” é a mesma ideia com um rótulo mais amigável: “apenas uma instância”, compartilhada por todos.
No código real, nem sempre isso é óbvio. Eles se escondem em variáveis no nível do módulo, “services” do framework, campos estáticos de classe ou helpers que guardam coisas em memória. Se você procura globals mutáveis compartilhadas, esse é o formato do que você deve procurar.
Nem tudo global é ruim. Constantes somente leitura geralmente são seguras: strings de versão, limites fixos e configurações padrão. O risco começa quando o valor pode mudar enquanto requisições estão em voo. Estado mutável somado à concorrência cria bugs que somem quando você coloca logs.
Estado compartilhado aparece frequentemente como:
- Caches em nível de módulo ou valores memoizados que guardam resultados específicos de usuário
- Um objeto de configuração compartilhado que é mutado (por exemplo, “tenant atual”)
- Um wrapper de cliente de DB compartilhado que também guarda dados por requisição como “current user”
- Helpers de auth/session que mantêm tokens na memória em vez de por requisição
- Filas na memória ou arrays de “jobs pendentes” usados por vários usuários
O perigo aumenta conforme você escala. Um servidor dev de usuário único pode parecer ok, mas múltiplos workers ou instâncias tornam o comportamento imprevisível. Algumas falhas só aparecem quando duas requisições se sobrepõem.
Se você herdou um protótipo gerado por IA, esses atalhos de “uma instância” são comuns. Durante uma auditoria, FixMyMess costuma encontrá‑los em torno de autenticação, cache e trabalho em background.
Sinais rápidos de que você tem estado compartilhado oculto
Estado compartilhado oculto geralmente se manifesta como problemas que parecem aleatórios. A app “funciona na maior parte do tempo”, então falha de maneiras que você não consegue reproduzir sob demanda.
O sintoma mais claro é vazamento de dados entre usuários. Uma requisição atualiza algo, e a próxima (de outro usuário ou tenant) vê isso. Você pode notar um usuário aparecendo de repente logado como outro, o nome da organização errado no cabeçalho, ou um dashboard carregando os dados de outro cliente por um instante.
O comportamento de testes é outro sinal. Um teste passa quando rodado sozinho, mas falha ao rodar a suíte completa. Isso costuma significar que um teste deixou estado para trás (como um currentUser em cache, ou um wrapper global de banco de dados com configurações mutáveis) que altera o próximo teste.
Tráfego piora. Quando requisições se sobrepõem durante um pico, você obtém 500 ocasionais que desaparecem ao tentar de novo. Esse padrão frequentemente aponta para objetos compartilhados sendo mutados no meio da requisição, como um objeto de config global, um cliente singleton com headers por requisição, ou uma variável de “requisição atual” em nível de módulo.
Um último sinal: “Funcionou localmente.” Muitos servidores dev tratam requisições uma a uma, então bugs de estado compartilhado permanecem ocultos até a produção rodar múltiplas requisições ao mesmo tempo.
Placas vermelhas rápidas para inspecionar:
- Uma variável em nível de módulo que muda durante a requisição (
token,tenantId,currentUser) - Um cliente singleton que armazena dados por requisição (headers, auth, locale)
- Caches em memória usados como fonte de verdade (não apenas para acelerar)
- Logs que mostram IDs de requisição ou usuário misturados no mesmo fluxo
- Bugs que somem quando você adiciona print statements (mudança de timing)
Times às vezes trazem para FixMyMess um protótipo onde duas pessoas fazem login ao mesmo tempo e as sessões se cruzam. Isso quase sempre é estado compartilhado, não “auth misteriosa”.
Como rastrear singletons ocultos passo a passo
Singletons ocultos são uma razão comum para comportamentos “aleatórios” sob carga: uma requisição muda algo, e a próxima herda. Abaixo um método prático para encontrar o responsável.
Busca passo a passo
Percorra sua base de código nesta ordem (economiza tempo e pega os maiores culpados primeiro):
- Procure por variáveis no nível do módulo que sejam reatribuídas (não só constantes). Fique de olho em nomes como
currentUser,token,client,configousession. - Caçe padrões de singleton:
getInstance(), comentários “criar apenas uma vez”, ou inicialização preguiçosa como “se não criado, cria agora”. - Inspecione caches e memoização. Um cache pode ser ok, mas fica perigoso se a chave for ausente, muito ampla (como só
userIdsemtenantId) ou se assumir uma chave padrão para todas as requisições. - Revise handlers e middleware procurando objetos armazenados fora do handler. Uma armadilha comum é criar um objeto na inicialização e depois mutá‑lo por requisição.
- Cheque os “locals” do framework e stores da app. Misturar locais de requisição com locais da app transforma dados por requisição em armazenamento entre requisições.
Verificação rápida de realidade
Para confirmar que achou o culpado: abra duas sessões de navegador (normal e anônima), logue como usuários diferentes e acesse o mesmo endpoint algumas vezes. Se identidades, permissões ou configurações vazarem entre sessões, você quase certamente ainda tem estado mutável compartilhado.
Um modelo simples: estado por requisição vs recursos compartilhados
Uma forma confiável de eliminar globais mutáveis compartilhadas é separar tudo em dois baldes: coisas que pertencem a uma requisição, e coisas seguras para compartilhar entre requisições.
Estado por requisição é tudo que muda de usuário para usuário ou chamada para chamada: o ID do usuário atual, claims de autenticação, um correlation ID para logs, locale e o payload de entrada. Esses dados nunca devem viver em variável de módulo, porque duas requisições podem se sobrepor e sobrescrever um ao outro.
Recursos compartilhados são blocos caros que você pode reutilizar com segurança: um pool de conexões ao banco, um cliente HTTP com configurações fixas, um template compilado. A chave é que esses objetos não devem armazenar dados específicos de requisição dentro deles.
Regra simples: se fosse errado a Requisição B ver aquilo, não pode ser global.
Modelo prático para manter a integridade:
- Coloque o estado por requisição em parâmetros de função ou em um pequeno objeto
RequestContext. - Construa dependências explicitamente usando construtores ou uma função fábrica.
- Mantenha objetos compartilhados imutáveis (configuração) ou internamente seguros (como um pool de DB).
- Se algo precisa ser compartilhado e mutável, proteja com sincronização adequada.
Exemplo: em vez de um currentUser global, crie ctx = { user, correlationId } quando a requisição começar, e passe ctx para handlers e serviços. Seu pool de DB permanece compartilhado, mas as funções de consulta recebem ctx para que logging e permissões fiquem corretos.
Plano de refatoração: mover de globals para estado explícito
Para substituir globais mutáveis com segurança, comece pequeno. Escolha uma área de risco onde o estado errado causa dor rápida, como auth, seleção de tenant ou caching. Uma mudança focalizada é mais fácil de revisar e menos provável de quebrar funcionalidades não relacionadas.
Primeiro, anote o que o código está puxando secretamente de “algum lugar”: usuário atual, tenant ID, feature flags, locale, request ID etc. Então crie um objeto de contexto de requisição minúsculo que contenha só o que você realmente precisa.
Sequência que funciona na maioria das bases de código:
- Escolha um ponto de entrada (uma rota de API, handler ou job) e construa o contexto da requisição ali.
- Mude uma função por vez para aceitar o contexto como argumento em vez de ler globals.
- Quando uma função precisa de um serviço (db, cache, auth client), passe‑o ou construa‑o a partir de uma fábrica.
- Mantenha recursos compartilhados compartilhados (como pool de conexão), mas mantenha dados por requisição separados.
- Quando funcionar, remova o global antigo ou faça‑o falhar alto se acessado.
Uma fábrica pode ser a ponte que evita uma grande reescrita. Por exemplo, createServices(ctx) pode retornar authService, tenantService e auditLogger que todos leem do contexto passado, não de variáveis em módulo. Também deixa as dependências visíveis em vez de implícitas.
Por fim, delete ou congele o global antigo. Não deixe “só por garantia.” Alguém vai usá‑lo de novo num conserto rápido.
Dependências com escopo de requisição sem overengineering
O objetivo é direto: crie o que o código precisa uma vez por requisição, passe explicitamente, e mantenha as partes realmente compartilhadas (como pools) somente leitura do ponto de vista do handler. Isso geralmente basta para parar bugs de concorrência sem criar um sistema gigante de dependências.
Mantenha o “container de requisição” pequeno. Trate‑o como um objeto simples que guarda só o que varia por requisição: o usuário atual, request ID, locale, feature flags e um relógio. Todo o resto deve ser um recurso compartilhado seguro para reutilizar.
Padrão prático em um web app:
- Inicialização da app: crie recursos compartilhados (pool de DB, cliente HTTP, configuração do logger)
- Por requisição: crie estado da requisição (user, request ID) e pequenos helpers que precisem disso
- Handler: aceite essas dependências como parâmetros, não via imports
Por exemplo, um handler de login pode construir um RequestContext e então passá‑lo para serviços como AuthService(ctx, db_pool, logger). O pool de DB é compartilhado, mas o contexto não. Isso impede que os dados de um usuário vazem para outra requisição quando duas rodarem simultaneamente.
Dependências compartilhadas tipicamente seguras incluem um pool de conexões de DB (não uma conexão única armazenada globalmente), um cliente HTTP sem headers por usuário embutidos, e configuração do logger (mas não um global mutável currentUser).
Jobs em background são onde as pessoas voltam a usar globals porque não há requisição. Trate jobs do mesmo jeito: crie um JobContext com um job ID e qualquer user/workspace ID que o job pertença, e passe‑o para a função do job. Se você só passar um valor, passe o contexto.
Erros comuns que pioram o problema
A forma mais rápida de desfazer sua refatoração é manter o global e tentar “resetá‑lo” a cada requisição. Isso parece seguro em testes de usuário único, mas quebra assim que duas requisições se sobrepõem. Se uma requisição resetar enquanto outra ainda usa, você obtém comportamento difícil de reproduzir.
Outra armadilha clássica é guardar o “usuário atual” numa variável global ou serviço singleton. Parece conveniente porque dá acesso de qualquer lugar, mas transforma cada requisição numa corrida. Você verá sintomas como usuários virando uns aos outros, permissões trocando, ou logs de auditoria com o ator errado.
Caches globais também são perigosos quando não incluem tenant ou user nas chaves. Um cache que usa só um product ID (ou pior, um único valor “latest”) pode vazar dados entre contas. O bug não parece de cache; parece “minha app às vezes mostra dados de outra pessoa.”
Alguns erros começam como hacks de performance e viram problemas de confiabilidade. Criar uma nova conexão de DB por requisição em vez de usar um pool pode esgotar conexões sob carga. Depois você “conserta” com retries e timeouts, o que esconde o problema real e torna as falhas mais difíceis de entender.
Misturar config mutável com estado de runtime é outro assassino silencioso. Se você muda um objeto de config em runtime (feature flags, base URLs, valores de ambiente) e esse objeto é compartilhado, cada requisição pode ver uma configuração diferente dependendo do timing.
Sinais vermelhos rápidos que costumam aparecer juntos:
- Um singleton tem campos como
currentUser,token,requestIdoulastResult - Uma chave de cache falta
tenantIdouuserId - Funções de “reset” rodando em middleware ou antes de handlers
- Conexões de DB abertas e fechadas dentro do handler
- Objetos de config modificados após a inicialização
Se você herdou código gerado por IA, esses padrões aparecem muito em protótipos. Muitas vezes só surgem quando usuários reais usam a app ao mesmo tempo.
Como confirmar a correção com testes simples
Depois de substituir globais mutáveis, o código costuma parecer melhor imediatamente. A prova real é que ele se mantém consistente quando duas coisas acontecem ao mesmo tempo.
1) Adicione um teste de concorrência pequeno
Você não precisa de uma suíte enorme. Comece com um teste que dispare duas requisições em paralelo usando usuários diferentes (ou chaves de API diferentes) e afirme que as respostas nunca se misturam.
# Pseudocode example
# Send two parallel requests:
# - user A logs in and fetches /me
# - user B logs in and fetches /me
# Assert A never sees B's data, and B never sees A's data.
Se esse teste falhar ao menos uma vez, ainda há estado compartilhado escondido em algum lugar.
2) Torne o cross-talk visível com logs
Adicione logs estruturados simples que incluam request ID e user ID em cada handler e em qualquer objeto de serviço que você refatorou. Então procure sequências impossíveis, como o request ID do usuário A logando o user ID do usuário B.
Algumas verificações rápidas que revelam acoplamento oculto:
- Rode a suíte com execução paralela habilitada.
- Execute o teste de concorrência em loop 50 a 200 vezes para capturar falhas não determinísticas.
- Adicione uma asserção de que objetos com escopo de requisição são criados por requisição (não reaproveitados).
- Monitore memória durante o loop para confirmar que fica estável após remover caches acidentais.
- Reduza temporariamente timeouts para fazer condições de corrida aparecerem antes.
Se ainda houver falhas aleatórias, normalmente há um singleton sobrando (como um cliente em nível de módulo que guarda “current user”) ou um cache com chave muito ampla.
Exemplo: um protótipo que quebra quando dois usuários fazem login
Um bug comum em protótipos gerados por IA parece inofensivo em testes de usuário único: estado de autenticação fica numa variável global. Por exemplo, a app mantém currentUser (ou um accessToken) em uma variável de módulo, e todas as rotas da API leem dali.
Aqui está o que acontece em produção. Usuário A faz login, depois Usuário B faz login um momento depois. O currentUser global é sobrescrito. Agora, quando o Usuário A clica em “Minha Conta”, o servidor às vezes responde como se fosse o Usuário B. Parece aleatório porque depende do timing, não do caminho do código.
Sinais típicos em logs e tickets de suporte:
- “Vi os dados de outra pessoa por um segundo”
- Requisições mostram o user ID errado mesmo com cookies corretos
- O problema só aparece sob carga ou quando duas pessoas testam juntas
- Atualizar a página às vezes “conserta”
A correção é parar de perguntar a um singleton global quem é o usuário atual. Em vez disso, construa um serviço de auth por requisição, usando contexto explícito (headers, cookies, session ID). Cada requisição recebe seu próprio objeto auth, e handlers o recebem como parâmetro.
Concretamente: parseie o token da requisição, verifique‑o, e passe o usuário verificado para as funções que precisam. Recursos compartilhados (como pool de DB) podem permanecer compartilhados, mas identidade do usuário deve ter escopo de requisição.
Após essa refatoração, logins concorrentes e chamadas de API ficam consistentes: Usuário A sempre vê os dados do Usuário A, mesmo com o Usuário B ativo.
Checklist rápido antes de liberar
Antes de dar deploy, faça uma última varredura para garantir que não deixou estado compartilhado exposto.
Verificações de prontidão para envio
- Procure no código de tratamento de requisição por variáveis em nível de módulo que mudam (qualquer coisa escrita durante uma requisição).
- Recheque caches e memoização. Garanta que chaves incluam o que separa usuários e tenants (e locale quando altera a saída).
- Confirme que o contexto da requisição é passado, não lido de singletons escondidos. Se uma função precisa do usuário atual, token, tenant ou timezone, ela deve receber como argumento (ou via dependência com escopo de requisição).
- Audite o que você compartilha. Pools de conexão, config imutável e clientes somente leitura geralmente são ok. Qualquer coisa com campos mutáveis (como
currentUser,lastQuery,headers) não é. - Rode um teste paralelo: dois usuários logando e executando ações diferentes ao mesmo tempo. Procure por dados entre usuários, sessões misturadas ou erros de permissão aleatórios.
Se você herdou um protótipo gerado por IA, vale fazer essa checklist duas vezes. Essas apps normalmente escondem current user global ou clientes compartilhados com headers mutáveis.
Próximos passos se seu código gerado por IA tem bugs de concorrência
Se sua app funciona com um usuário só mas falha com tráfego real, trate como problema de estado compartilhado até provar o contrário. Muitos projetos gerados por IA mantêm dados em memória que deveriam viver na requisição, sessão ou banco.
Comece fazendo um inventário rápido de tudo que pode ser escrito por mais de uma requisição. Procure variáveis em nível de módulo, objetos em cache que guardam dados de usuário, “singletons” criados na inicialização e utilitários que mantêm estado interno.
Uma maneira simples de avançar é consertar uma jornada de usuário fim a fim. Escolha o caminho que mais quebra (login, checkout, upload). Refatore apenas esse caminho para que o estado seja passado por argumentos de função ou dependências com escopo de requisição. Quando um caminho estiver limpo, os padrões ficam mais fáceis e seguros de repetir.
Quando não souber onde está o singleton oculto, estreite a busca:
- Adicione logs com IDs de objetos e user IDs em passos chave (auth, acesso ao DB, caching)
- Faça grep por “global”, “singleton”, “cache”, “memo”, “static” e atribuições no nível de módulo
- Desative temporariamente cache em memória para ver se o bug some
Às vezes é mais rápido pedir uma revisão de especialista, especialmente quando o código mistura frameworks, jobs em background e auth customizado. FixMyMess (fixmymess.ai) diagnostica e repara código gerado por IA, começando com uma auditoria gratuita de código. A maioria dos projetos é concluída em 48–72 horas com ferramentas assistidas por IA e verificação humana especializada, para que as correções aguentem a carga.
Perguntas Frequentes
What exactly is a “shared mutable global”?
Um global mutável compartilhado é qualquer valor que vive fora de uma requisição ou job específico e que pode ser alterado enquanto a aplicação está rodando. Ele se torna arriscado quando várias requisições podem ler e escrever nele, porque o trabalho de um usuário pode sobrescrever o estado de outro.
Why do shared globals cause bugs that feel random?
Porque o resultado passa a depender do momento (timing), não apenas do caminho do código. Sob requisições paralelas, retries ou trabalho em background, duas operações podem se sobrepor e reaproveitar o mesmo estado, fazendo o bug aparecer e sumir conforme a carga e o agendamento.
What are common real-world examples of hidden globals or singletons?
Fique atento a coisas como um currentUser em nível de módulo, um cliente singleton que armazena headers ou tokens, um “cache global” com resultados por usuário ou um objeto de configuração compartilhado que é mutado (por exemplo, “tenant atual”). Esses padrões costumam funcionar em testes de um único usuário e falhar quando requisições se sobrepõem.
What are the clearest signs that state is leaking between requests?
Se usuários virem a conta errada, o nome do tenant errado ou dados de outro usuário, trate como estado compartilhado até provar o contrário. Tests instáveis que dependem da ordem e falhas de autenticação ocasionais (401/403) também são sinais fortes.
What’s a quick way to reproduce or confirm the problem?
Abra duas sessões (por exemplo, uma janela normal e uma anônima), faça login com dois usuários diferentes e acesse os mesmos endpoints repetidamente. Se identidade, permissões ou configurações cruzarem entre sessões, há dados específicos da requisição vivendo na memória compartilhada em algum lugar.
What can be shared safely, and what should never be shared?
Recursos compartilhados são seguros quando não contêm dados por requisição. Pool de conexões, um cliente HTTP com configurações fixas e configuração imutável costumam ser seguros; o problema começa quando o objeto compartilhado guarda campos mutáveis como currentUser, token, tenantId ou headers por requisição.
How do I replace a global `currentUser` without rewriting everything?
Crie um pequeno contexto de requisição no ponto de entrada (handler ou middleware) contendo só o que muda por requisição, como identidade do usuário e um request ID. Em seguida, passe esse contexto (ou serviços derivados dele) para as funções em vez de importar um global que carrega estado silenciosamente.
How can I keep caching without leaking data across users?
Caching é aceitável quando serve só como camada de desempenho, não como fonte única da verdade. O essencial é escopo e chaves corretas: inclua tenant e user quando os resultados variarem por tenant/usuário, e evite um único valor “latest” que qualquer requisição possa sobrescrever.
How should I handle background jobs if there’s no “request”?
Trate um job como uma requisição: crie um JobContext com o ID do job e quaisquer identificadores de workspace/usuário que o job pertence, e passe isso para a função do job. Evite ler ou escrever estado em nível de módulo dentro de job runners, já que múltiplos jobs podem rodar ao mesmo tempo.
What should I do if I inherited an AI-generated prototype that breaks under load?
Comece com uma auditoria focada em auth, caching e clientes singleton, pois esses são pontos frequentes de falha em protótipos gerados por IA. Se quiser agilizar, FixMyMess pode rodar uma auditoria gratuita de código para identificar o estado compartilhado oculto e então reparar e reforçar o código; muitos projetos são concluídos em 48–72 horas.