Diseña un sistema de feature flags / configuración remota para una app móvil con millones de usuarios
Recorre el SDK del cliente, la entrega respaldada por CDN, el bucketing determinista y el camino del kill-switch que jamás puede esperar a una publicación en la tienda.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Diseña un sistema de feature flags / configuración remota para una app móvil con millones de usuarios
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Primero, aclara los requisitos
Antes de dibujar cajas, un candidato senior determina para qué existe el sistema de flags, porque esa respuesta define toda la arquitectura. Bajo el término "feature flag" se esconden al menos tres casos de uso distintos: el kill switch operativo (apagar un flujo de pago roto en segundos, sin pasar por una revisión de tienda), el rollout gradual (lanzar una función al 1% de usuarios, observar la tasa de crashes, y subir hasta el 100%), y la experimentación (probar A/B dos flujos de checkout y medir conversión). Cada uno tiene un requisito de latencia distinto: un kill switch necesita propagarse en segundos, un porcentaje de rollout tolera minutos, y un experimento solo necesita consistencia por usuario, no velocidad. También preguntaría por la escala (decenas de millones de dispositivos activos diarios), la granularidad del targeting (versión de app, sistema operativo, idioma, atributos personalizados del usuario) y, de forma crítica, qué pasa cuando el dispositivo arranca sin red, porque ese último caso es el que más diseños fallan.
#Arquitectura de alto nivel
El sistema se divide claramente en un plano de control y un plano de datos. El plano de control es donde producto e ingeniería definen flags, reglas de targeting y porcentajes de rollout mediante un dashboard; ahí se escribe un documento de configuración inmutable y versionado. El plano de datos es lo que realmente llega a millones de dispositivos, y nunca debe ser "llamar directamente a un servidor de API" — a esa escala se necesita una CDN o caché de borde delante de un número pequeño de payloads JSON estáticos y versionados, no una base de datos absorbiendo millones de lecturas por minuto.
#El SDK del cliente: local-first por diseño
El contrato del SDK con la app es simple: las lecturas deben ser síncronas, no bloqueantes, y siempre deben devolver algo, incluso en un arranque en frío sin red. Eso significa que la última configuración obtenida con éxito se persiste en disco, y el arranque de la app lee ese snapshot persistido antes de que se resuelva cualquier llamada de red. Una actualización en segundo plano refresca luego el snapshot para la próxima vez. Es el mismo patrón "última versión conocida buena" que usarías para cualquier caché entre la app y una red poco fiable: nunca bloquees el hilo de UI esperando una configuración remota.
interface FeatureFlagClient {fun isEnabled(key: String, default: Boolean = false): Booleanfun <T> getVariant(key: String, default: T): Tfun refresh(force: Boolean = false)}class RemoteConfigClient(private val store: FlagStore, // payload persistido, última versión buenaprivate val fetcher: ConfigFetcher, // cliente HTTP respaldado por CDN, soporta ETagprivate val context: EvaluationContext // deviceId estable, userId, appVersion, idioma) : FeatureFlagClient {@Volatile private var snapshot: ConfigSnapshot = store.loadCached() ?: ConfigSnapshot.empty()override fun isEnabled(key: String, default: Boolean): Boolean {val rule = snapshot.flags[key] ?: return defaultreturn evaluate(rule) as? Boolean ?: default}override fun <T> getVariant(key: String, default: T): T {val rule = snapshot.flags[key] ?: return default@Suppress("UNCHECKED_CAST")return evaluate(rule) as? T ?: default}private fun evaluate(rule: FlagRule): Any? {if (!rule.matchesTargeting(context)) return rule.defaultValueval bucket = stableBucket(context.stableId, rule.key) // 0..9999val variant = rule.rollout.firstOrNull { bucket < it.cumulativeUpperBound }return variant?.value ?: rule.defaultValue}override fun refresh(force: Boolean) {fetcher.fetch(etag = if (force) null else snapshot.etag) { result ->when (result) {is FetchResult.NotModified -> {} // el TTL se renueva, nada cambióis FetchResult.Updated -> { snapshot = result.snapshot; store.persist(snapshot) }is FetchResult.Failed -> {} // seguimos sirviendo la última versión buena}}}}// Determinista: el mismo dispositivo + la misma flag caen siempre en el mismo bucketfun stableBucket(stableId: String, flagKey: String): Int {val hash = murmur3_32("$stableId:$flagKey".toByteArray())return (hash.toUInt() % 10_000u).toInt()}
#El problema del kill switch: la latencia de la CDN es una ventaja, hasta que no lo es
Cachear en CDN con un TTL de 1 a 5 minutos es exactamente correcto para un porcentaje de rollout: intercambia algo de desactualización por una escalabilidad enorme. Es incorrecto para un kill switch de emergencia, donde "espera hasta cinco minutos" puede significar cinco minutos de un checkout roto cobrando tarjetas dos veces. La solución es desacoplar un conjunto pequeño de flags de alta prioridad del TTL del bloque general de configuración: un canal de push ligero (una notificación silenciosa de APNs/FCM) le dice a la app "algo cambió, vuelve a pedirlo ya", saltándose por completo la espera de la caché. Esta es la parte del diseño que más exploran los entrevistadores, porque es fácil despacharla con un "simplemente baja el TTL", algo que no escala a millones de clientes haciendo polling cada pocos segundos.
#Modelo de datos, versionado y rollback
Cada definición de flag lleva una clave, un tipo, un valor por defecto, una lista ordenada de reglas de targeting y una cascada de rollout (bandas de porcentaje acumulado que mapean a variantes). Como el payload publicado es inmutable y versionado, el rollback no es "volver a desplegar": es apuntar el alias de la CDN a la versión anterior, algo rápido y seguro precisamente porque nada muta en su lugar. También mantendría los entornos (staging/producción) completamente separados y registraría una auditoría de quién cambió qué, ya que un flag mal configurado es efectivamente un incidente de producción.
#Seguridad, registro de exposición y el camino rápido para QA
Los flags evaluados en el dispositivo son visibles para cualquiera que inspeccione el tráfico de la app, así que el payload de configuración nunca debe llevar secretos, solo toggles de comportamiento y valores no sensibles. Para que la experimentación produzca estadísticas válidas, el SDK debe registrar un evento de exposición en el momento en que una flag realmente se evalúa en la sesión de un usuario, no simplemente cuando se obtuvo la configuración; confundir "obtenido" con "expuesto" es un bug sutil que corrompe silenciosamente cualquier test A/B construido encima. Por último, dale a ingeniería y QA un panel de override local para forzar cualquier variante sin esperar a un rollout real: la mayoría de los incidentes de producción que he visto con sistemas de flags vienen de que nadie pudo probar el estado "apagado" antes de que se publicara.