Saltar al contenido principal

Feature Flags & Progressive Delivery 🔴

Los feature flags (también llamados feature toggles) permiten activar o desactivar funcionalidad en producción sin hacer un nuevo deploy. Son la base del progressive delivery: canary releases, A/B testing y dark launches.


¿Por qué feature flags?

Sin feature flags:
Code → Merge → Deploy → Todos los usuarios ven el cambio

Si hay un bug, rollback = nuevo deploy (lento)

Con feature flags:
Code → Merge → Deploy → Flag OFF → 0 usuarios afectados
→ Flag ON 1% → canary
→ Flag ON 50% → A/B test
→ Flag ON 100% → rollout completo
→ Flag OFF → rollback instantáneo

Tipos de feature flags

TipoDuraciónCaso de uso
Release toggleDías/semanasOcultar feature incompleta mientras se desarrolla
Experiment toggleSemanasA/B testing, medir impacto de cambios
Ops toggleMeses/permanenteKill switch para funcionalidad en producción
Permission togglePermanenteAcceso diferenciado por plan/rol (premium features)

1. Implementación en .NET con Microsoft.FeatureManagement

La forma nativa en .NET, sin dependencias externas:

dotnet add package Microsoft.FeatureManagement.AspNetCore

Configuración básica

// Program.cs
builder.Services.AddFeatureManagement();

// O con filtros de contexto
builder.Services.AddFeatureManagement()
.AddFeatureFilter<PercentageFilter>() // Porcentaje de usuarios
.AddFeatureFilter<TimeWindowFilter>() // Ventana de tiempo
.AddFeatureFilter<TargetingFilter>(); // Targeting por usuario/grupo
// appsettings.json
{
"FeatureManagement": {
"NuevoCheckout": true,
"RecomendacionesIA": false,
"DashboardV2": {
"EnabledFor": [
{
"Name": "Percentage",
"Parameters": {
"Value": 20
}
}
]
},
"BetaFeature": {
"EnabledFor": [
{
"Name": "TimeWindow",
"Parameters": {
"Start": "2026-04-01T00:00:00",
"End": "2026-04-30T23:59:59"
}
}
]
}
}
}

Uso en código

// ✅ En un servicio
public class PedidoService
{
private readonly IFeatureManager _featureManager;

public PedidoService(IFeatureManager featureManager)
{
_featureManager = featureManager;
}

public async Task<PedidoDto> ProcesarAsync(PedidoRequest request)
{
if (await _featureManager.IsEnabledAsync("NuevoCheckout"))
{
return await ProcesarConNuevoFlujAsync(request);
}

return await ProcesarConFlujoClasico(request);
}
}

// ✅ En un controller — attribute
[FeatureGate("NuevoCheckout")] // Retorna 404 si el flag está OFF
[HttpGet("nuevo-checkout")]
public IActionResult NuevoCheckout()
{
return Ok("Nuevo checkout activo");
}

// ✅ En una view/razor
@inject IFeatureManager FeatureManager

@if (await FeatureManager.IsEnabledAsync("DashboardV2"))
{
<partial name="_DashboardV2" />
}
else
{
<partial name="_Dashboard" />
}

// ✅ Con enum (evita strings mágicos)
public static class Features
{
public const string NuevoCheckout = "NuevoCheckout";
public const string RecomendacionesIA = "RecomendacionesIA";
public const string DashboardV2 = "DashboardV2";
}

await _featureManager.IsEnabledAsync(Features.NuevoCheckout);

2. Targeting — Flags por usuario/grupo

El targeting permite activar flags para usuarios específicos o grupos antes de un rollout global:

// appsettings.json
{
"FeatureManagement": {
"NuevaUI": {
"EnabledFor": [
{
"Name": "Targeting",
"Parameters": {
"Audience": {
"Users": ["alice@empresa.com", "bob@empresa.com"],
"Groups": [
{
"Name": "beta-testers",
"RolloutPercentage": 100
},
{
"Name": "empleados",
"RolloutPercentage": 50
}
],
"DefaultRolloutPercentage": 5
}
}
}
]
}
}
}
// Configurar el contexto de targeting (quién es el usuario actual)
builder.Services.AddFeatureManagement()
.AddFeatureFilter<TargetingFilter>();

builder.Services.AddSingleton<ITargetingContextAccessor, HttpContextTargetingContextAccessor>();

// Implementar el accessor
public class HttpContextTargetingContextAccessor : ITargetingContextAccessor
{
private readonly IHttpContextAccessor _httpContextAccessor;

public HttpContextTargetingContextAccessor(IHttpContextAccessor accessor)
{
_httpContextAccessor = accessor;
}

public ValueTask<TargetingContext> GetContextAsync()
{
var user = _httpContextAccessor.HttpContext?.User;

return new ValueTask<TargetingContext>(new TargetingContext
{
UserId = user?.FindFirst(ClaimTypes.Email)?.Value ?? "anonymous",
Groups = user?.Claims
.Where(c => c.Type == "grupo")
.Select(c => c.Value)
.ToList() ?? new List<string>()
});
}
}

3. Feature flags en React (frontend)

// ✅ Obtener flags del backend al iniciar la app
interface FeatureFlags {
nuevoCheckout: boolean;
recomendacionesIA: boolean;
dashboardV2: boolean;
}

// Context para flags
const FeatureFlagContext = createContext<FeatureFlags>({
nuevoCheckout: false,
recomendacionesIA: false,
dashboardV2: false,
});

export function FeatureFlagProvider({ children }: { children: React.ReactNode }) {
const [flags, setFlags] = useState<FeatureFlags | null>(null);

useEffect(() => {
fetch('/api/feature-flags') // Endpoint que retorna flags del usuario actual
.then(r => r.json())
.then(setFlags);
}, []);

if (!flags) return <LoadingSpinner />;

return (
<FeatureFlagContext.Provider value={flags}>
{children}
</FeatureFlagContext.Provider>
);
}

// ✅ Hook para consumir flags
export function useFeatureFlag(flag: keyof FeatureFlags): boolean {
const flags = useContext(FeatureFlagContext);
return flags[flag];
}

// ✅ Componente de guardia
export function FeatureFlag({
name,
children,
fallback = null,
}: {
name: keyof FeatureFlags;
children: React.ReactNode;
fallback?: React.ReactNode;
}) {
const enabled = useFeatureFlag(name);
return enabled ? <>{children}</> : <>{fallback}</>;
}

// ✅ Uso en componentes
function Checkout() {
const nuevoCheckout = useFeatureFlag('nuevoCheckout');

return nuevoCheckout ? <NuevoCheckoutFlow /> : <CheckoutClasico />;
}

// O con el componente guard
function App() {
return (
<FeatureFlag name="dashboardV2" fallback={<DashboardV1 />}>
<DashboardV2 />
</FeatureFlag>
);
}

4. Endpoint de flags para el frontend

// ✅ Controller que expone flags al frontend
[ApiController]
[Route("api/feature-flags")]
[Authorize]
public class FeatureFlagsController : ControllerBase
{
private readonly IFeatureManager _featureManager;

public FeatureFlagsController(IFeatureManager featureManager)
{
_featureManager = featureManager;
}

[HttpGet]
public async Task<IActionResult> GetFlags()
{
// Los flags se evalúan en contexto del usuario actual (via TargetingContextAccessor)
var flags = new
{
nuevoCheckout = await _featureManager.IsEnabledAsync(Features.NuevoCheckout),
recomendacionesIA = await _featureManager.IsEnabledAsync(Features.RecomendacionesIA),
dashboardV2 = await _featureManager.IsEnabledAsync(Features.DashboardV2),
};

return Ok(flags);
}
}

5. Estrategias de rollout progresivo

Estrategia canary (porcentaje incremental):

Día 1: 1% → Sin alertas → OK
Día 2: 5% → Sin alertas → OK
Día 3: 20% → Sin alertas → OK
Día 5: 50% → Sin alertas → OK
Día 7: 100% → Rollout completo

Si en cualquier punto hay anomalías:
→ Flag OFF → 0% instantáneamente sin deploy
// Patrón: Dark Launch
// La feature se activa para TODOS pero los usuarios no la ven
// Se ejecuta en paralelo con la feature antigua para medir impacto

public async Task<RecomendacionesDto> ObtenerRecomendacionesAsync(int usuarioId)
{
var resultadoClasico = await _recomendacionClasica.ObtenerAsync(usuarioId);

if (await _featureManager.IsEnabledAsync(Features.RecomendacionesIA))
{
try
{
// Ejecutar nueva feature en paralelo pero NO retornar a usuario aún
var resultadoIA = await _recomendacionIA.ObtenerAsync(usuarioId);

// Solo loguear para comparar resultados
_logger.LogInformation(
"DarkLaunch - Clasico: {Classic}, IA: {AI}",
resultadoClasico.Count, resultadoIA.Count);

// Métricas de calidad del nuevo algoritmo
_metrics.Track("recommendations.ia.quality", CalcularCalidad(resultadoIA));
}
catch (Exception ex)
{
_logger.LogWarning(ex, "DarkLaunch IA falló, no afecta al usuario");
}
}

return resultadoClasico; // Siempre retorna el resultado clásico por ahora
}

6. Azure App Configuration (flags centralizados)

Para múltiples servicios o equipos grandes, los flags deben ser centralizados:

dotnet add package Microsoft.Azure.AppConfiguration.AspNetCore
// Program.cs
builder.Configuration.AddAzureAppConfiguration(options =>
{
options.Connect(connectionString)
.UseFeatureFlags(ff =>
{
ff.Select("*"); // Todos los flags
ff.CacheExpirationInterval = TimeSpan.FromSeconds(30); // Refresh cada 30s
});
});

builder.Services.AddAzureAppConfiguration();
builder.Services.AddFeatureManagement();

// En middleware pipeline
app.UseAzureAppConfiguration(); // Habilita refresh automático

Con Azure App Configuration puedes:

  • Cambiar flags en tiempo real sin reiniciar la app
  • Gestionar flags de múltiples servicios desde un solo lugar
  • Auditar quién cambió qué flag y cuándo

Antipatrones que evitar

❌ Flag zombie: flags que permanecen meses después del rollout completo
→ Agregar fecha de expiración a cada flag, revisión periódica

❌ Demasiados flags anidados:
if (flagA && flagB && !flagC) { ... }
→ Confuso, difícil de testear

❌ Lógica de negocio dentro del flag:
if (await IsEnabled("PrecioEspecial")) { precio *= 0.9m; }
→ El flag debe activar una ruta, no contener la lógica

❌ Flags sin tests:
→ Testea siempre ambos paths (flag ON y flag OFF)

Testing con feature flags

// ✅ Testear ambos estados del flag
public class PedidoServiceTests
{
[Fact]
public async Task ProcesarPedido_FlagNuevoCheckoutON_UsaNuevoFlujo()
{
var featureManager = new Mock<IFeatureManager>();
featureManager
.Setup(x => x.IsEnabledAsync("NuevoCheckout"))
.ReturnsAsync(true);

var service = new PedidoService(featureManager.Object, ...);

await service.ProcesarAsync(new PedidoRequest());

// Verificar que se usó el nuevo flujo
}

[Fact]
public async Task ProcesarPedido_FlagNuevoCheckoutOFF_UsaFlujoClasico()
{
var featureManager = new Mock<IFeatureManager>();
featureManager
.Setup(x => x.IsEnabledAsync("NuevoCheckout"))
.ReturnsAsync(false);

// ...
}
}

Preguntas frecuentes de entrevista 🎯

1. ¿Qué es un feature flag y cuándo lo usarías?

Un feature flag es un condicional en el código que permite activar o desactivar funcionalidad sin hacer un deploy. Lo usaría para: rollouts progresivos, A/B testing, kill switches de emergencia, y separar el deploy del release.

2. ¿Cuál es la diferencia entre "deploy" y "release"?

Deploy es llevar el código a producción. Release es hacer la feature visible a los usuarios. Los feature flags permiten deployar código constantemente (CI/CD) y elegir cuándo hacer el release.

3. ¿Cómo evitarías la deuda técnica de feature flags?

Cada flag debe tener una fecha de expiración y un dueño. Al hacer rollout al 100%, se crea un ticket para remover el flag y el código del path antiguo. Sin disciplina, acaban siendo "flag zombies" que nadie toca.

4. ¿Qué es un canary release?

Activar una nueva feature para un pequeño porcentaje de usuarios (1-5%) antes del rollout completo. Permite detectar bugs en producción con impacto mínimo. Si hay anomalías, el flag se apaga instantáneamente.

5. ¿Cómo testeas código con feature flags?

Mockeas IFeatureManager y ejecutas los tests para ambos estados del flag (ON y OFF). Nunca dejes un path sin testear — es exactamente lo que pasa en producción.