26 dic 2025·8 min de lectura

Reemplaza globals mutables compartidos por estado explícito de forma segura

Aprende a reemplazar globals mutables compartidos identificando singletons ocultos y moviendo el estado a dependencias con alcance por petición para evitar condiciones de carrera.

Reemplaza globals mutables compartidos por estado explícito de forma segura

Por qué los globals mutables compartidos causan errores raros y aleatorios

“Mutable compartido” suena elegante, pero es simple: un valor vive en un solo lugar (compartido) y tu código puede cambiarlo (mutable). Cuando muchas partes de una app leen y escriben ese mismo valor, el comportamiento empieza a depender del timing, no de la intención.

Por eso estos errores parecen aleatorios. Con un solo usuario navegando, la app puede verse bien. Bajo peticiones paralelas, jobs en segundo plano o reintentos automáticos, dos operaciones pueden tocar el mismo global al mismo tiempo. Una petición actualiza el valor y otra petición lo usa por accidente, aunque sean usuarios o tareas diferentes.

Un ejemplo común es una variable currentUser almacenada en un módulo, una “cache global” que también guarda datos por petición, o un cliente singleton que silenciosamente mantiene estado (headers, tokens, un tenant seleccionado). Estos son exactamente los casos donde debes reemplazar globals mutables compartidos por estado explícito.

Los síntomas típicos se ven así:

  • Los usuarios ven los datos de otra persona o se loguean como la persona equivocada
  • “Funciona en mi máquina” que solo falla bajo carga
  • Tests inestables que pasan o fallan según el orden
  • Errores 401/403 aleatorios porque se reutilizó el token equivocado
  • Jobs en segundo plano que usan la configuración equivocada

El objetivo no es “no compartir nada”. Bases de datos y pools de conexiones se comparten a propósito. El objetivo es: los recursos compartidos siguen siendo compartidos, mientras que el estado específico de la petición se pasa (o se crea por petición) para que nada se reutilice silenciosamente.

Esto aparece mucho en prototipos generados por IA. FixMyMess a menudo encuentra singletons ocultos que parecen inofensivos hasta que llega tráfico real.

Qué cuenta como global o singleton en proyectos reales

Un “global” es cualquier fragmento de estado que vive fuera de una petición, job o acción de usuario específica, pero que aún se lee y escribe mientras la app corre. Un “singleton” es la misma idea con un nombre más elegante: “solo una instancia”, compartida por todos.

En código real no siempre son obvios. Se esconden en variables a nivel de módulo, “servicios” del framework, campos estáticos de clases o helpers que silenciosamente guardan cosas en memoria. Si buscas globals mutables compartidos, esta es la forma de lo que estás buscando.

No todo lo global es malo. Las constantes de solo lectura suelen ser seguras: cadenas de versión, límites fijos y configuraciones por defecto. El riesgo empieza cuando el valor puede cambiar mientras las peticiones están en vuelo. Estado mutable más concurrencia crea los bugs que desaparecen cuando agregas logs.

El estado compartido suele aparecer como:

  • Caches a nivel de módulo o valores memoizados que guardan resultados específicos de usuario
  • Un objeto de config compartido que se muta (por ejemplo, “tenant actual”)
  • Un wrapper de cliente DB compartido que también almacena datos por petición como “current user”
  • Helpers de auth/session que mantienen tokens en memoria en lugar de por petición
  • Colas en memoria o arrays de “jobs pendientes” usados por varios usuarios

El peligro aumenta a medida que escalas. Un servidor de desarrollo de un solo usuario puede verse bien, pero múltiples workers o instancias hacen el comportamiento menos predecible. Algunas fallas solo aparecen cuando dos peticiones se solapan.

Si heredaste un prototipo generado por IA, estos atajos de “una instancia” son comunes. En una auditoría, FixMyMess suele encontrarlos alrededor de autenticación, caching y trabajo en segundo plano.

Señales rápidas de que tienes estado compartido oculto

El estado compartido oculto suele manifestarse como problemas que se sienten aleatorios. La app “funciona en su mayoría”, luego falla de formas que no puedes reproducir a demanda.

El síntoma más claro es que los datos se mezclan entre usuarios. Una petición actualiza algo y la siguiente petición (de un usuario o tenant distinto) lo ve. Podrías notar que un usuario aparece de repente logueado como otro, el nombre de la organización equivocado en el encabezado, o un dashboard que carga brevemente los datos de otro cliente.

El comportamiento en tests es otra pista. Un test pasa si lo ejecutas solo, pero falla cuando corres todo el suite. Eso suele significar que un test deja estado detrás (como una currentUser cacheada, o un wrapper global de DB con settings mutables) que cambia el siguiente test.

El tráfico empeora la situación. Cuando las peticiones se solapan durante un pico, obtienes 500 ocasionales que desaparecen al reintentar. Ese patrón suele apuntar a objetos compartidos que se mutan a mitad de la petición, como un objeto config global, un cliente singleton con headers por petición, o una variable de “petición actual” a nivel de módulo.

Una señal final: “Funcionó localmente.” Muchos servidores de desarrollo manejan peticiones de una en una, así que los bugs de estado compartido permanecen ocultos hasta que en producción se atienden varias peticiones a la vez.

Red flags rápidas para escanear:

  • Una variable a nivel de módulo que cambia durante una petición (token, tenantId, currentUser)
  • Un cliente singleton que almacena datos por petición (headers, auth, locale)
  • Caches en memoria usados como fuente de verdad (no solo como acelerador)
  • Logs que muestran IDs de petición o usuario mezclados en el mismo flujo
  • Bugs que desaparecen cuando agregas print statements (cambios de timing)

Equipos a veces traen a FixMyMess un prototipo donde dos personas inician sesión al mismo tiempo y las sesiones se cruzan. Eso casi siempre es estado compartido, no “auth misteriosa”.

Cómo rastrear singletons ocultos paso a paso

Los singletons ocultos son una razón común por la que ves comportamientos “aleatorios” bajo carga: una petición cambia algo y la siguiente lo hereda.

Búsqueda paso a paso

Recorre tu base de código en este orden (ahorra tiempo y atrapa a los mayores culpables primero):

  • Escanea variables a nivel de módulo que se reasignan (no solo constantes). Fíjate en nombres como currentUser, token, client, config o session.
  • Busca patrones de singleton: getInstance(), comentarios tipo “crear solo una vez” o inicialización perezosa como “si no está creado, crear ahora”.
  • Inspecciona caches y memoización. Una cache puede estar bien, pero se vuelve peligrosa si la clave falta, es demasiado amplia (por ejemplo solo userId sin tenantId) o por defecto usa una clave única para todas las peticiones.
  • Revisa handlers de petición y middleware en busca de objetos almacenados fuera del handler. Una trampa común es crear un objeto una vez al arrancar y luego mutarlo por petición.
  • Comprueba “locals” y stores globales del framework. Confundir request locals con app locals convierte datos por petición en almacenamiento entre peticiones.

Una comprobación rápida de realidad

Para confirmar que encontraste al culpable: abre dos sesiones del navegador (normal e incógnito), inicia sesión como dos usuarios distintos y llama al mismo endpoint unas cuantas veces. Si las identidades, permisos o settings se filtran entre sesiones, casi seguro aún tienes estado mutable compartido.

Un modelo simple: estado por petición vs recursos compartidos

Una manera fiable de eliminar globals mutables compartidos es dividir todo en dos cubos: cosas que pertenecen a una petición, y cosas que es seguro compartir entre peticiones.

El estado por petición es cualquier cosa que cambia de usuario a usuario o llamada a llamada: el ID de usuario actual, claims de auth, un correlation ID para logs, locale y la payload específica. Estos datos nunca deberían vivir en una variable a nivel de módulo, porque dos peticiones pueden solaparse y sobrescribirse mutuamente.

Los recursos compartidos son piezas costosas que puedes reutilizar con seguridad: un pool de conexiones a la DB, un cliente HTTP con settings fijos, una plantilla compilada. La clave es que estos objetos no deben almacenar dentro de sí datos específicos de la petición.

Una regla simple: si estaría mal que la Petición B lo vea, no puede ser global.

Un modelo práctico que te mantiene honesto:

  • Pon el estado por petición en parámetros de función o en un pequeño objeto RequestContext.
  • Construye dependencias explícitamente usando constructores o una función fábrica.
  • Mantén los objetos compartidos inmutables (config) o internamente seguros (como un pool de DB).
  • Si algo debe ser compartido y mutable, protégelo con sincronización adecuada.

Ejemplo: en lugar de un currentUser global, crea ctx = { user, correlationId } cuando inicia la petición y pasa ctx a handlers y servicios. Tu pool DB sigue siendo compartido, pero las funciones de consulta reciben ctx para que logs y permisos sean correctos.

Plan de refactor: mover de globals a estado explícito

Evita fugas de datos entre usuarios
Si los usuarios alguna vez ven datos incorrectos, localizaremos la causa antes de que cometas más cambios.

Para reemplazar globals mutables compartidos de forma segura, empieza pequeño. Elige un área arriesgada donde el estado equivocado duela rápido, como auth, selección de tenant o caching. Un cambio focalizado es más fácil de revisar y menos probable que rompa funciones no relacionadas.

Primero, escribe qué está tomando el código secretamente de “alguna parte”: current user, tenant ID, feature flags, locale, request ID, etc. Luego crea un pequeño objeto de contexto de petición que contenga solo lo que realmente necesitas.

Una secuencia que funciona en la mayoría de codebases:

  • Elige un punto de entrada (una ruta API, handler o job) y construye el contexto de petición allí.
  • Cambia una función a la vez para que acepte el contexto como argumento en lugar de leer globals.
  • Cuando una función necesite un servicio (db, cache, auth client), pásalo o constrúyelo desde una fábrica.
  • Mantén recursos compartidos compartidos (como un pool de conexiones), pero mantén los datos por petición por petición.
  • Cuando funcione, elimina el global antiguo o haz que falle ruidosamente si se accede.

Una fábrica puede ser el puente que evita una gran reescritura. Por ejemplo, createServices(ctx) puede devolver authService, tenantService y auditLogger que lean del contexto pasado, no de variables a nivel de módulo. También hace visibles las dependencias en lugar de implícitas.

Finalmente, borra o congela el global antiguo. No lo dejes “por si acaso”. Alguien lo usará de nuevo en un parche rápido.

Dependencias con alcance por petición sin sobreingeniería

El objetivo es sencillo: crea lo que tu código necesita una vez por petición, pásalo explícitamente y mantiene las partes realmente compartidas (como pools) de solo lectura desde el punto de vista del handler. Eso suele ser suficiente para detener bugs de concurrencia sin construir un enorme sistema de dependencias.

Mantén el “contenedor de petición” pequeño. Trátalo como un objeto plano que contiene solo lo que varía por petición: el usuario actual, request ID, locale, feature flags y un reloj. Todo lo demás debe ser un recurso compartido seguro de reutilizar.

Un patrón práctico en una web app:

  • Inicio de la app: crea recursos compartidos (DB pool, cliente HTTP, configuración del logger)
  • Por petición: crea estado de petición (usuario, request ID) y pequeños helpers que lo necesiten
  • Handler: acepta estas dependencias como parámetros, no mediante imports

Por ejemplo, un handler de login puede construir un RequestContext una vez y luego pasarlo a servicios como AuthService(ctx, db_pool, logger). El pool DB es compartido, pero el contexto no. Eso evita que los datos de un usuario se filtren a otra petición cuando ambas corren simultáneamente.

Dependencias compartidas normalmente seguras incluyen un pool de conexiones a la BD (no una conexión única almacenada globalmente), un cliente HTTP sin headers por usuario integrados y la configuración del logger (pero no un currentUser mutable global).

Los jobs en segundo plano son donde la gente vuelve a usar globals porque no hay una petición. Trata los jobs igual: crea un JobContext con un job ID y cualquier user/workspace ID que pertenezca al job y pásalo a la función. Si solo pasas un valor, pasa el contexto.

Errores comunes que empeoran el problema

Llega a producción en días
Desde el diagnóstico hasta la preparación para despliegue, te ayudamos a terminar en 48–72 horas.

La forma más rápida de deshacer tu propio refactor es mantener el global e intentar “resetearlo” en cada petición. Eso parece seguro en pruebas de un solo usuario, pero se rompe cuando dos peticiones se solapan. Si una petición resetea el valor mientras otra aún lo usa, obtienes comportamientos difíciles de reproducir.

Otra trampa clásica es almacenar el “usuario actual” en una variable global o en un servicio singleton. Se siente conveniente porque puedes accederlo desde cualquier parte, pero convierte cada petición en una carrera. Verás síntomas como usuarios que se convierten entre sí, permisos que cambian o logs de auditoría con el actor equivocado.

Caches globales también son riesgosas cuando no incluyen tenant o user en la clave. Una cache que usa solo un product ID (o peor, un único valor “latest”) puede filtrar datos entre cuentas. El bug no parece de cache; parece “mi app a veces muestra datos de otra persona”.

Algunos errores empiezan como hacks de rendimiento pero crean problemas de confiabilidad. Abrir una nueva conexión DB por petición en lugar de usar un pool puede agotar conexiones bajo carga. Entonces “lo arreglas” con reintentos y timeouts, lo que oculta el problema real y hace que las fallas sean más difíciles de razonar.

Mezclar config mutable con estado en tiempo de ejecución es otro asesino silencioso. Si cambias un objeto de config en tiempo de ejecución (feature flags, URLs base, valores de entorno) y ese objeto es compartido, cada petición puede ver una configuración distinta según el timing.

Red flags rápidas que suelen aparecer juntas:

  • Un singleton guarda campos como currentUser, token, requestId o lastResult
  • Una clave de cache no incluye tenantId o userId
  • Funciones de “reset” corren en middleware o antes de handlers
  • Conexiones DB se abren y cierran en el handler de la petición
  • Objetos de config se modifican después del arranque

Si heredas código generado por IA, estos patrones aparecen mucho en prototipos. A menudo solo se manifiestan cuando usuarios reales golpean la app al mismo tiempo.

Cómo confirmar la corrección con tests simples

Después de reemplazar globals mutables compartidos, el código suele sentirse mejor de inmediato. La prueba real es que se mantenga consistente cuando dos cosas suceden a la vez.

1) Añade un test de concurrencia pequeño

No necesitas una suite enorme. Empieza con un test que lance dos peticiones en paralelo usando usuarios distintos (o claves API distintas) y aserte que sus respuestas nunca se mezclan.

# 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.

Si este test falla alguna vez, todavía tienes estado compartido oculto en alguna parte.

2) Haz visible el cross-talk con logs

Añade logs estructurados sencillos que incluyan request ID y user ID en cada handler y en cualquier objeto de servicio que refactorizaste. Luego busca secuencias imposibles, como el request ID de A registrando el ID de usuario de B.

Algunas comprobaciones rápidas que sacan a la luz el acoplamiento oculto:

  • Corre el suite de tests con ejecución paralela habilitada.
  • Itera el test de concurrencia 50 a 200 veces para atrapar fallos no deterministas.
  • Añade una aserción de que los objetos con scope de petición se crean por petición (no se reutilizan).
  • Monitorea memoria durante el bucle para confirmar que se mantiene estable tras eliminar caches accidentales.
  • Baja temporalmente los timeouts para que las condiciones de carrera aparezcan antes.

Si aún ves fallos aleatorios, normalmente significa un singleton restante (como un cliente a nivel de módulo que guarda “current user”) o una cache con clave demasiado amplia.

Ejemplo: un prototipo que falla cuando dos usuarios inician sesión

Un bug común en prototipos generados por IA parece inofensivo en pruebas de un solo usuario: el estado de autenticación se guarda en una variable global. Por ejemplo, la app mantiene currentUser (o un accessToken) en una variable a nivel de módulo y todas las rutas API leen de ella.

Esto es lo que pasa en producción. El usuario A inicia sesión y luego el usuario B inicia sesión un momento después. El currentUser global se sobrescribe. Ahora, cuando el usuario A hace clic en “Mi cuenta”, el servidor a veces responde como si fuera el usuario B. Parece aleatorio porque depende del timing, no de la ruta de código.

Señales típicas en logs y tickets de soporte:

  • “Vi los datos de otra persona por un segundo”
  • Las peticiones muestran el ID de usuario equivocado aunque las cookies parezcan correctas
  • El problema solo aparece bajo carga o cuando dos personas prueban a la vez
  • Refrescar a veces “lo arregla”

La solución es dejar de preguntar a un singleton global quién es el usuario actual. En su lugar, construye un servicio de auth por petición usando contexto explícito (headers, cookies, session ID). Cada petición obtiene su propio objeto auth y los handlers lo reciben como parámetro.

Concretamente: parsea el token de la petición entrante, verifícalo y luego pasa el usuario verificado a las funciones que lo necesitan. Los recursos compartidos (como un pool DB) pueden seguir compartidos, pero la identidad del usuario debe ser scoped por petición.

Tras este refactor, los logins concurrentes y llamadas API son consistentes: el usuario A siempre ve sus datos, incluso cuando el usuario B está activo.

Lista de comprobación rápida antes de publicar

Depura fallos intermitentes en producción
Diagnosticamos por qué “funciona en mi máquina” falla en producción y te ayudamos a desplegar con confianza.

Antes de publicar, haz una pasada final para asegurarte de que no dejaste estado compartido escondido a la vista.

Comprobaciones para producción

  • Busca en el código de manejo de peticiones variables a nivel de módulo que cambien (cualquier cosa escrita durante una petición).
  • Revisa caches y memoización. Asegúrate de que las claves incluyan lo que separa usuarios y tenants (y locale cuando cambia la salida).
  • Confirma que el contexto de petición se pasa como argumento, no se lee desde singletons ocultos. Si una función necesita el usuario actual, token de auth, tenant o zona horaria, debe recibirlo como argumento (o a través de una dependencia con scope de petición).
  • Audita lo que compartes. Pools de conexión, config inmutable y clientes de solo lectura suelen estar bien. Cualquier cosa con campos mutables (como currentUser, lastQuery, headers) no lo está.
  • Ejecuta un test paralelo: dos usuarios iniciando sesión y realizando acciones simultáneamente. Busca datos cruzados entre usuarios, sesiones mezcladas o errores de permiso aleatorios.

Si heredaste un prototipo generado por IA, vale la pena hacer esta lista dos veces. Estas apps suelen colar estado global de “usuario actual” o clientes compartidos con headers mutables.

Próximos pasos si tu código generado por IA tiene bugs de concurrencia

Si tu app funciona con un usuario pero falla bajo tráfico real, trátalo como un problema de estado compartido hasta que se demuestre lo contrario. Muchos proyectos generados por IA mantienen accidentalmente datos en memoria que deberían vivir en una petición, sesión o base de datos.

Empieza haciendo un inventario rápido de todo lo que puede escribirse desde más de una petición. Busca variables a nivel de módulo, objetos cacheados que guarden datos de usuario, “singletons” creados al inicio y utilidades que mantengan estado interno.

Una forma simple de avanzar es arreglar un flujo de usuario completo de extremo a extremo. Elige el camino que más rompe (login, checkout, subida de archivos). Refactoriza solo esa ruta para que el estado se pase por argumentos de función o dependencias con scope de petición. Una vez que un camino esté limpio, los patrones son más fáciles y seguros de replicar.

Cuando no sabes dónde está el singleton oculto, reduce la búsqueda:

  • Añade logs de IDs de objetos y IDs de usuario en pasos clave (auth, acceso a DB, caching)
  • Grep por “global”, “singleton”, “cache”, “memo”, “static” y asignaciones a nivel de módulo
  • Desactiva temporalmente el caching en memoria para ver si el bug desaparece

A veces es más rápido obtener una revisión experta, especialmente cuando el código mezcla frameworks, jobs en segundo plano y auth personalizada. FixMyMess (fixmymess.ai) diagnostica y repara código generado por IA, empezando con una auditoría de código gratuita. La mayoría de proyectos se completan en 48–72 horas con herramientas asistidas por IA y verificación humana experta, de modo que las correcciones resisten la carga.

Preguntas Frecuentes

¿Qué es exactamente un “global mutable compartido”?

Un global mutable compartido es cualquier valor que vive fuera de una petición o trabajo específico y puede cambiar mientras la app está en ejecución. Se vuelve riesgoso cuando varias peticiones pueden leerlo y escribirlo, porque el trabajo de un usuario puede sobrescribir el estado de otro.

¿Por qué los globales compartidos causan errores que parecen aleatorios?

Porque el resultado depende del momento (timing), no solo del flujo de código. Bajo peticiones paralelas, reintentos o trabajos en segundo plano, dos operaciones pueden solaparse y reutilizar el mismo estado por accidente, así que el error aparece y desaparece según la carga y la planificación.

¿Cuáles son ejemplos reales comunes de globals o singletons ocultos?

Fíjate en cosas como un currentUser a nivel de módulo, un cliente singleton que guarda headers o tokens, una “cache global” que contiene resultados por usuario, o un objeto de configuración compartido que se muta (por ejemplo, “tenant actual”). Estos patrones suelen funcionar en pruebas de un solo usuario y fallan cuando las peticiones se solapan.

¿Cuáles son las señales más claras de que el estado se está filtrando entre peticiones?

Si los usuarios ven alguna vez la cuenta equivocada, el nombre del tenant equivocado o datos de otro usuario, trátalo como estado compartido hasta que se demuestre lo contrario. Tests inestables que dependen del orden y fallos de autenticación como 401/403 aleatorios también son señales fuertes.

¿Cuál es una forma rápida de reproducir o confirmar el problema?

Abre dos sesiones (por ejemplo, una normal y una privada), inicia sesión como dos usuarios distintos y llama repetidamente al mismo endpoint. Si la identidad, permisos o configuraciones cruzan de una sesión a otra, todavía tienes datos específicos de la petición viviendo en memoria compartida en algún lugar.

¿Qué se puede compartir con seguridad y qué nunca debería compartirse?

Los recursos compartidos están bien cuando no contienen datos por petición. Un pool de conexiones a la base de datos, un cliente HTTP con configuración fija y una configuración inmutable suelen ser seguros; el peligro empieza cuando el objeto compartido almacena campos mutables como currentUser, token, tenantId o headers por petición.

¿Cómo reemplazo un `currentUser` global sin reescribir todo?

Crea un pequeño contexto de petición en el punto de entrada (handler o middleware) que contenga solo lo que cambia por petición, como identidad del usuario y un request ID. Después pasa ese contexto (o servicios derivados creados a partir de él) a las funciones en lugar de importar un global que lleva estado de forma implícita.

¿Cómo mantengo el caching sin filtrar datos entre usuarios?

El caching está bien como capa de rendimiento, no como fuente oculta de verdad. La clave es el scope y las claves correctas: incluye tenant y usuario cuando los resultados difieren por tenant/usuario, y evita un único valor “latest” que cualquier petición pueda sobrescribir.

¿Cómo debo manejar los jobs en segundo plano si no hay una “petición”?

Trata un job como una petición: crea un JobContext con el ID del job y cualquier identificador de workspace/usuario que corresponda, y pásalo a la función del job. Evita leer o escribir estado a nivel de módulo dentro de los runners de jobs, porque varios jobs pueden ejecutarse simultáneamente.

¿Qué hago si heredé un prototipo generado por IA que falla bajo carga?

Empieza con una auditoría enfocada en autenticación, caching y clientes singleton, porque suelen ser los puntos de falla en prototipos generados por IA. Si quieres ir más rápido, FixMyMess puede hacer una auditoría de código gratuita para identificar el estado compartido oculto y luego reparar y endurecer el código; la mayoría de proyectos se completan en 48–72 horas.