Feature flags a escala: estrategias para entrega progresiva
Implementa feature flags con despliegues por porcentaje, segmentación, pruebas A/B y kill switches, desacoplando el despliegue del lanzamiento.

El despliegue no es el lanzamiento
Desplegar código y lanzar una funcionalidad son dos acciones distintas. El despliegue es una operación técnica: subir artefactos a producción. El lanzamiento es una decisión de negocio: exponer funcionalidad a los usuarios. Los feature flags separan ambas cosas, permitiéndote desplegar código oscuro en cualquier momento y lanzar funcionalidades cuando estés listo, a quien tú elijas.
El contrato del feature flag
Un feature flag es un punto de decisión en tiempo de ejecución. En su forma más simple, es un booleano. A escala, es un motor de reglas que evalúa el contexto del usuario contra reglas de segmentación para devolver un valor.
interface FeatureFlag {
key: string;
type: "boolean" | "string" | "number" | "json";
defaultValue: unknown;
enabled: boolean;
rules: TargetingRule[];
killSwitch: boolean;
}
interface TargetingRule {
priority: number;
conditions: Condition[];
percentage: number;
value: unknown;
}
interface Condition {
attribute: string;
operator: "eq" | "neq" | "in" | "contains" | "gt" | "lt" | "semver_gt";
value: unknown;
}
interface EvaluationContext {
userId: string;
email?: string;
plan?: string;
country?: string;
appVersion?: string;
customAttributes?: Record<string, unknown>;
}class FeatureFlagClient {
private flags: Map<string, FeatureFlag> = new Map();
evaluate<T>(flagKey: string, context: EvaluationContext, fallback: T): T {
const flag = this.flags.get(flagKey);
// Flag doesn't exist — return fallback
if (!flag) return fallback;
// Kill switch activated — return default immediately
if (flag.killSwitch) return flag.defaultValue as T;
// Flag disabled globally — return default
if (!flag.enabled) return flag.defaultValue as T;
// Evaluate targeting rules in priority order
const sortedRules = [...flag.rules].sort(
(a, b) => a.priority - b.priority
);
for (const rule of sortedRules) {
if (this.matchesConditions(rule.conditions, context)) {
if (this.isInPercentage(context.userId, flagKey, rule.percentage)) {
return rule.value as T;
}
}
}
return flag.defaultValue as T;
}
private matchesConditions(
conditions: Condition[],
context: EvaluationContext
): boolean {
return conditions.every((condition) => {
const contextValue = this.getAttribute(context, condition.attribute);
switch (condition.operator) {
case "eq":
return contextValue === condition.value;
case "neq":
return contextValue !== condition.value;
case "in":
return Array.isArray(condition.value) &&
condition.value.includes(contextValue);
case "contains":
return typeof contextValue === "string" &&
contextValue.includes(condition.value as string);
default:
return false;
}
});
}
// Deterministic percentage based on userId + flagKey hash
private isInPercentage(
userId: string,
flagKey: string,
percentage: number
): boolean {
if (percentage >= 100) return true;
if (percentage <= 0) return false;
const hash = this.consistentHash(`${userId}:${flagKey}`);
return (hash % 100) < percentage;
}
}Patrones de lanzamiento progresivo
Empieza con usuarios internos, expande a beta testers, y luego sube a 1%, 5%, 25%, 50%, 100%. Si las métricas empeoran en cualquier etapa, detén o revierte al instante — sin necesidad de desplegar.
// ❌ Big-bang release — all users get the feature at once
function NewCheckout() {
return <RadicallyDifferentCheckout />;
}
// ✅ Progressive rollout with monitoring at each stage
function Checkout() {
const flags = useFeatureFlags();
const isNewCheckout = flags.evaluate("new-checkout-flow", {
userId: user.id,
plan: user.plan,
country: user.country,
}, false);
return isNewCheckout ? <NewCheckoutFlow /> : <CurrentCheckoutFlow />;
}
// Rollout configuration evolution:
const rolloutStages: TargetingRule[] = [
// Stage 1: Internal team only
{
priority: 1,
conditions: [{ attribute: "email", operator: "contains", value: "@ourcompany.com" }],
percentage: 100,
value: true,
},
// Stage 2: Beta users
{
priority: 2,
conditions: [{ attribute: "plan", operator: "eq", value: "beta" }],
percentage: 100,
value: true,
},
// Stage 3: 5% of all users
{
priority: 3,
conditions: [],
percentage: 5,
value: true,
},
// Stage 4: 50% of all users
// Stage 5: 100% — flag cleaned up
];Evaluación en servidor vs cliente
Dónde evalúes los flags afecta latencia, seguridad y confiabilidad. La evaluación del lado del servidor mantiene las reglas de segmentación privadas y garantiza consistencia. La evaluación del cliente reduce la latencia para cambios de UI pero expone las configuraciones de los flags.
// Server-side: evaluate on the API, send only the result
// app/api/flags/route.ts
export async function GET(request: Request) {
const context = await buildContextFromRequest(request);
// Only send evaluated values, never rules or conditions
const flags = {
"new-checkout": flagClient.evaluate("new-checkout", context, false),
"dark-mode": flagClient.evaluate("dark-mode", context, false),
"pricing-tier": flagClient.evaluate("pricing-tier", context, "standard"),
};
return Response.json(flags);
}
// Client-side: bootstrap with server-evaluated values
function FlagProvider({ children }: { children: React.ReactNode }) {
const [flags, setFlags] = useState<Record<string, unknown>>({});
useEffect(() => {
fetch("/api/flags")
.then((r) => r.json())
.then(setFlags);
}, []);
return (
<FlagContext.Provider value={flags}>
{children}
</FlagContext.Provider>
);
}
function useFlag<T>(key: string, fallback: T): T {
const flags = useContext(FlagContext);
return (flags[key] as T) ?? fallback;
}Ciclo de vida del flag y deuda técnica
Los feature flags que viven para siempre se convierten en deuda técnica. Cada flag es una rama en tu código. Establece un ciclo de vida: crear, lanzar, alcanzar el 100%, eliminar el flag y la ruta de código antigua.
interface FlagMetadata {
key: string;
owner: string;
createdAt: string;
expectedRemovalDate: string;
purpose: "release" | "experiment" | "ops" | "permission";
jiraTicket: string;
}
// Automated flag staleness detection
function findStaleFlags(
flags: FlagMetadata[],
today: Date = new Date()
): FlagMetadata[] {
return flags.filter((flag) => {
const removalDate = new Date(flag.expectedRemovalDate);
return removalDate < today;
});
}
// ESLint rule concept: warn on flags past their removal date
// Flag inventory tracked alongside code
const FLAG_REGISTRY: FlagMetadata[] = [
{
key: "new-checkout-flow",
owner: "payments-team",
createdAt: "2025-06-01",
expectedRemovalDate: "2025-08-01",
purpose: "release",
jiraTicket: "PAY-1234",
},
{
key: "maintenance-mode",
owner: "platform-team",
createdAt: "2025-01-15",
expectedRemovalDate: "9999-12-31", // Permanent ops flag
purpose: "ops",
jiraTicket: "PLAT-500",
},
];Monitoreo y observabilidad
Cada evaluación de un flag debería emitir telemetría. Sin ella, no puedes correlacionar los estados de los flags con tasas de error, cambios de latencia o métricas de negocio.
class ObservableFeatureFlagClient extends FeatureFlagClient {
constructor(private readonly metrics: MetricsClient) {
super();
}
evaluate<T>(flagKey: string, context: EvaluationContext, fallback: T): T {
const start = performance.now();
const result = super.evaluate(flagKey, context, fallback);
const duration = performance.now() - start;
this.metrics.increment("feature_flag.evaluation", {
flag: flagKey,
result: String(result),
userId: context.userId,
});
this.metrics.histogram("feature_flag.evaluation_ms", duration, {
flag: flagKey,
});
return result;
}
}Conclusiones clave
Los feature flags desacoplan el despliegue del lanzamiento. Despliega código en cualquier momento; lanza funcionalidades a usuarios específicos, en porcentajes específicos, con un interruptor — no con un despliegue. Usa lanzamientos progresivos para limitar el radio de impacto: primero internos, luego beta, después sube los porcentajes con monitoreo en cada etapa.
Evalúa los flags del lado del servidor cuando las reglas de segmentación sean sensibles o la consistencia importe. Rastrea cada flag con metadatos — propietario, propósito, fecha esperada de eliminación — y exige la limpieza para evitar la acumulación de deuda técnica. Emite telemetría en cada evaluación para poder correlacionar los estados de los flags con métricas de producción.
El objetivo no es tener más flags. El objetivo es lanzar con confianza, sabiendo que cualquier funcionalidad nueva puede revertirse en segundos sin tocar el pipeline de despliegue.


