-
Notifications
You must be signed in to change notification settings - Fork 1
Feature Flags
Los Feature Flags (o feature toggles) son una técnica que permite desacoplar el despliegue de código del lanzamiento de funcionalidades. Linker1 utiliza LaunchDarkly como plataforma de gestión de feature flags.
LaunchDarkly es una plataforma de feature management que permite:
- Activar/desactivar features en tiempo real sin redeployar
- Targeting por usuario o segmento: habilitar features para grupos específicos
- Rollout gradual: liberar features al 10%, 50%, 100% de los usuarios
- Kill switch instantáneo: deshabilitar una feature en segundos si causa problemas
Linker1 usa el Java Server SDK (v7.8.0) para evaluación server-side de flags.
La integración con LaunchDarkly sigue el mismo patrón de Inyección de Dependencias (DI) e Inversión de Control (IoC) usado en todo el proyecto:
Main (Composition Root)
│
├── LDClient (LaunchDarkly SDK)
│ │
│ ▼
├── FeatureFlags(ldClient) ← ÚNICO punto de contacto con el SDK
│ │
│ ▼
└── StaticRoutes(featureFlags) ← Consume flags sin saber del SDK
-
FeatureFlagses la ÚNICA clase que importa o interactúa con el SDK de LaunchDarkly - Los consumidores (e.g.,
StaticRoutes) recibenFeatureFlagspor constructor y llaman métodos simples comoisNewUiEnabled() - Los tests inyectan un
FeatureFlagscon valores predefinidos — sin conexión al SDK
Controlar qué versión de la interfaz web se sirve a los usuarios:
| Flag State | Interfaz | Archivos |
|---|---|---|
OFF (default) |
V1 — Interfaz original |
v1/index.html + v1/styles.css
|
ON |
V2 — Nueva interfaz "Colombia Edition" |
v2/index-v2.html + v2/styles2.css
|
FeatureFlags.java — Evaluación del flag:
public boolean isNewUiEnabled() {
if (ldClient == null) {
return false; // safe default si no hay conexión
}
try {
LDContext context = LDContext.builder("anonymous-user")
.anonymous(true)
.build();
return ldClient.boolVariation("new-ui", context, false);
} catch (Exception e) {
return false; // fail-safe: si hay error, servir la UI estable
}
}StaticRoutes.java — Consumo del flag:
app.get("/", ctx -> {
String path = featureFlags.isNewUiEnabled()
? "/v2/index-v2.html" // nueva UI
: "/v1/index.html"; // UI original
serve(ctx, path, "text/html", notFound);
});
app.get("/styles.css", ctx -> {
String path = featureFlags.isNewUiEnabled()
? "/v2/styles2.css" // estilos nuevos
: "/v1/styles.css"; // estilos originales
serve(ctx, path, "text/css", notFound);
});| Variable de entorno | Propósito | Obligatoria |
|---|---|---|
LD_SDK_KEY |
Clave del SDK de LaunchDarkly | Sí — la app no arranca sin ella |
LD_OFFLINE |
Modo offline para testing local | No (default: false) |
Si LD_SDK_KEY no está definida, Main.java ejecuta System.exit(1):
String ldSdkKey = System.getenv("LD_SDK_KEY");
if (ldSdkKey == null || ldSdkKey.isBlank()) {
System.err.println("ERROR: The LD_SDK_KEY environment variable is not set.");
System.exit(1);
}El modo offline (LD_OFFLINE=true) permite ejecutar la app sin conectar al servicio de LaunchDarkly (todos los flags retornan su valor default).
Cada nueva feature que use feature flags debe seguir este proceso:
┌─────────────────────────────────────────────────────────────────┐
│ 1. DEVELOP Implementar la feature envuelta en un flag │
│ (flag OFF por defecto) │
│ │
│ 2. DEPLOY Desplegar a producción con el flag OFF │
│ (código nuevo, feature invisible) │
│ │
│ 3. TEST Habilitar para testing interno │
│ (targeting a developers/QA) │
│ │
│ 4. CANARY Habilitar para 10% de usuarios │
│ (progressive delivery) │
│ │
│ 5. ROLLOUT Habilitar para 100% │
│ (feature fully released) │
│ │
│ 6. CLEANUP Eliminar código legacy + flag del codebase │
│ y del dashboard de LaunchDarkly │
└─────────────────────────────────────────────────────────────────┘
Durante la transición de la UI V1 a V2:
- Ambas versiones coexisten en
public/v1/ypublic/v2/ - El flag
new-uidecide cuál servir en cada request -
app.jses compartido (no depende del flag)
Una vez validada la V2 en producción al 100%:
- Eliminar
v1/index.htmlyv1/styles.css - Renombrar
v2/index-v2.html→index.htmlyv2/styles2.css→styles.css - Eliminar la lógica condicional en
StaticRoutes.java - Eliminar el check
featureFlags.isNewUiEnabled()y su implementación - Archivar el flag
new-uien LaunchDarkly
Si no se hace el cleanup, el flag viejo se convierte en deuda técnica: código muerto que confunde, bifurcaciones que nadie entiende, y tests adicionales que mantener.
| Beneficio | Cómo se evidencia en Linker1 |
|---|---|
| Releases seguros | La V2 se despliega con el flag OFF, sin riesgo |
| Rollback instantáneo | Un toggle en el dashboard revierte la feature en segundos |
| Testing en producción | La V2 se puede probar con tráfico real antes del lanzamiento |
| Bajo acoplamiento |
StaticRoutes no sabe que LaunchDarkly existe |
| Testabilidad | Tests unitarios inyectan un FeatureFlags mock |
| Continuous Delivery | El código se mergea a main continuamente, protegido por el flag |
A medida que la aplicación evolucione, se planean nuevos flags:
| Flag | Propósito |
|---|---|
strict-url-validation |
Validación más estricta de URLs |
custom-alias |
Disponibilidad de aliases personalizados |
experimental-api |
Nuevos endpoints REST para early adopters |
Para ejecutar la app localmente con feature flags:
Option 1: Con LaunchDarkly (requiere SDK key)
export LD_SDK_KEY="sdk-xxxxxxxx"
java -jar linker1-1.0-jar-with-dependencies.jarOption 2: Modo Offline (sin conexión a LaunchDarkly)
export LD_SDK_KEY="fake-key"
export LD_OFFLINE="true"
java -jar linker1-1.0-jar-with-dependencies.jarEn modo offline, todos los flags retornan su valor default (false para new-ui).
VS Code launch.json:
{
"type": "java",
"name": "Launch Linker (Main)",
"request": "launch",
"mainClass": "Main",
"env": {
"LD_SDK_KEY": "sdk-xxxxxxxx"
}
}