Ingeniería, arquitectura y performance — directo de quien construye
inicio/ blog/ artículo
Feature Flags · 5 min de lectura

Feature Flags y Multi-Tenancy: Cómo Pruebo en Producción Sin Romper Clientes

Cómo uso feature flags para llevar código sin terminar a producción sin PRs gigantes, y cómo una arquitectura multi-tenant me permite activar features solo en un tenant de prueba — probando en producción sin afectar a los demás clientes.

TT
TryTechDesarrollo de Software

El problema: PRs gigantes y ramas que se pudren

Cualquiera que haya trabajado en una feature grande conoce la sensación. Abres una rama, empiezas a implementar y tres semanas después tienes un PR con 4.000 líneas cambiadas en 60 archivos. Nadie revisa eso con atención. La rama divergió tanto de main que hacer merge se convierte en horas de resolver conflictos.

Peor aún: mientras la feature no está lista, vive aislada en una rama. No se integra con el resto del sistema, no corre en el entorno de producción, no recibe datos reales. Solo descubres que rompiste algo el día del merge — el peor día posible para descubrirlo.

La raíz del problema es una confusión común: hacer deploy de código y liberar una feature son cosas distintas. Las tratamos como si fueran lo mismo, y ahí es donde vive el sufrimiento.


La idea: desacoplar el deploy del release

Las feature flags (o feature toggles) resuelven exactamente esto. La idea es simple: envuelves el código nuevo en un condicional que consulta una flag. Si la flag está apagada, el código nuevo no corre — aunque esté en producción.

if (features.isEnabled("nuevo-checkout")) {
  return renderNuevoCheckout(cart);
}
return renderCheckoutAntiguo(cart);

Con esto, puedo llevar código sin terminar a main y a producción con la flag apagada. El código está ahí, desplegado, conviviendo con el resto del sistema — pero invisible para los usuarios. Esto lo cambia todo:

  • PRs pequeños y frecuentes. En vez de un PR de 4.000 líneas, hago diez PRs de 400 líneas a lo largo de dos semanas. Cada uno pasa por un review de verdad, se mergea rápido y nunca diverge mucho de main.
  • Integración continua de verdad. El código nuevo ya está integrado con el sistema desde el primer commit. No existe el traumático “día del merge”.
  • Kill switch instantáneo. Si algo sale mal con una feature ya activa, apago la flag. No necesito rollback, redeploy, nada. Un toggle y volvió al estado anterior.

Dónde vive la flag

La implementación puede ser tan simple o tan sofisticada como quieras. Al principio, una flag puede ser solo una variable de entorno o una fila en una tabla de configuración:

type FeatureContext = {
  tenantId: string;
  userId?: string;
};

class FeatureFlags {
  async isEnabled(flag: string, ctx: FeatureContext): Promise<boolean> {
    const config = await this.loadFlag(flag);
    if (!config) return false;

    // Encendida globalmente
    if (config.enabledGlobally) return true;

    // Encendida para tenants específicos
    if (config.enabledTenants?.includes(ctx.tenantId)) return true;

    return false;
  }
}

El punto importante es que la decisión se toma en runtime, consultando un estado que controlo sin necesidad de hacer deploy. Encender o apagar una flag es una operación de datos, no de código.


El superpoder del multi-tenant: probar en producción de verdad

Aquí entra la parte que, para mi caso, es la más valiosa. Mi sistema es fuertemente multi-tenant — varios clientes comparten la misma aplicación y la misma infraestructura, aislados lógicamente por un tenantId.

Esto significa que la feature flag no tiene que ser un interruptor global de encendido/apagado. Puede estar segmentada por tenant. Y eso es exactamente lo que me permite hacer algo que suena peligroso pero en la práctica es muy seguro: probar en producción.

Mantengo un tenant de prueba — un cliente ficticio que vive en la misma base de datos, la misma aplicación, los mismos servidores que los clientes reales. Cuando termino una feature, activo la flag solo para ese tenant:

// Flag encendida solo para el tenant de prueba
{
  flag: "nuevo-checkout",
  enabledGlobally: false,
  enabledTenants: ["tenant-qa-interno"]
}

Ahora la feature corre en producción — infraestructura real, datos reales en el formato real, latencia real, integraciones reales con pasarelas de pago y servicios externos. Pero solo es visible para el tenant de prueba. Los demás clientes siguen viendo exactamente lo que veían antes, sin saber que hay código nuevo corriendo justo al lado de ellos.

graph TB
  subgraph prod["Producción — misma aplicación, misma base de datos"]
    F{{"feature flag: nuevo-checkout"}}
    T1["Tenant A (cliente real)"]
    T2["Tenant B (cliente real)"]
    T3["Tenant C (cliente real)"]
    QA["Tenant QA (prueba)"]
  end

  F -->|"apagada"| T1
  F -->|"apagada"| T2
  F -->|"apagada"| T3
  F -->|"encendida"| QA

Cuando gano confianza en que la feature es sólida, hago un rollout gradual: la activo para un cliente piloto que aceptó probar, después para el 10% de los tenants, después para todos. Si aparece algún problema en el camino, apago la flag para ese segmento e investigo — sin afectar a quienes ya estaban contentos.


Por qué esto es mejor que un staging tradicional

Un entorno de staging intenta imitar producción, pero nunca llega. El volumen de datos es menor, las integraciones externas suelen estar en modo sandbox, la configuración diverge con el tiempo. Bugs que solo aparecen con datos reales y escala real pasan desapercibidos.

Probar en producción con un tenant de prueba elimina esa distancia. No hay “imitación”: es el entorno real, con un blast radius controlado por diseño. El aislamiento por tenant, que la aplicación ya garantiza para separar a los clientes, se convierte también en la frontera de seguridad de mis pruebas.

Claro, esto exige disciplina. Algunas reglas que sigo:

  • El tenant de prueba nunca dispara efectos secundarios reales hacia terceros — correos, cobros de verdad, webhooks hacia la producción de otros. Uso cuentas de sandbox en los servicios externos para ese tenant.
  • Cada flag tiene dueño y fecha de vencimiento. Una feature flag olvidada se convierte en deuda técnica. Una vez que la feature está 100% liberada, elimino la flag y el código muerto del camino antiguo.
  • La flag es observable. Logs y métricas registran qué camino de código corrió, para saber qué pasó cuando algo sale mal.

El resultado

Cambiar “rama larga + PR gigante + día del merge” por “PRs pequeños + código detrás de una flag + prueba en un tenant real” cambió por completo mi ritmo. Entrego más rápido, con menos miedo, y con un mecanismo de reversión que cuesta un clic en vez de un redeploy.

Las feature flags no son magia — trasladan parte de la complejidad del proceso de branching al runtime de la aplicación. Pero cuando el sistema ya es multi-tenant, obtienes gratis la pieza más valiosa: un lugar seguro dentro de la propia producción para ver el código nuevo funcionando de verdad, antes de que cualquier cliente real lo toque.