Qué es el patrón Flyweight y por qué importa
El patrón Flyweight es un patrón estructural que busca reducir el consumo de memoria y el costo de creación de objetos cuando una aplicación necesita manejar grandes cantidades de instancias muy parecidas. En lugar de crear un objeto nuevo cada vez, Flyweight propone compartir objetos comunes e inmutables, y separar de ellos el estado que cambia según el contexto. En términos prácticos, suele implementarse con una fábrica que aplica una estrategia getOrCreate: si el objeto ya existe, lo reutiliza; si no existe, lo crea, lo almacena y luego lo devuelve.
Este patrón resulta especialmente útil en sistemas con catálogos repetidos, estilos visuales, configuraciones, caracteres renderizados, conexiones lógicas, metadatos o cualquier escenario donde miles de objetos comparten una parte importante de su información. La idea clave no es solo ahorrar memoria: también mejora la consistencia del sistema, evita trabajo duplicado y puede disminuir la presión sobre el recolector de basura.
Cómo pensar Flyweight: estado intrínseco y extrínseco
Para entender Flyweight, conviene separar los datos de un objeto en dos grupos. El estado intrínseco es la parte compartible, estable y reutilizable. Por ejemplo, el nombre de una fuente, un color institucional o la configuración de un tipo de notificación. El estado extrínseco es la parte que cambia por uso: posición, cantidad, usuario actual, timestamp o contexto de ejecución. Flyweight comparte solo el estado intrínseco y recibe el extrínseco desde afuera cuando hace falta operar.
- Identifica qué datos se repiten mucho entre objetos.
- Separa la información compartible de la que depende del contexto.
- Crea una fábrica o repositorio con getOrCreate.
- Devuelve siempre la misma instancia cuando la clave sea igual.
- Mantén los objetos compartidos lo más inmutables posible.
Cuándo conviene usar Flyweight
Flyweight tiene sentido cuando el sistema crea muchos objetos similares y eso genera desperdicio de memoria o de CPU. También es una buena opción cuando la identidad del objeto puede definirse por una clave clara, como un código de idioma, un tipo de documento, un estilo visual, una categoría de producto o una combinación pequeña de atributos repetidos.
- Interfaces gráficas con miles de elementos visuales.
- Editores de texto o motores de renderizado.
- Sistemas de notificaciones con plantillas compartidas.
- Catálogos con metadatos repetidos.
- Motores de juegos con tipos de entidades reutilizables.
- Backends que reconstruyen objetos equivalentes una y otra vez.
Ejemplo en TypeScript: estilos de etiquetas en un dashboard
Imagina un dashboard de monitoreo que muestra miles de etiquetas de estado: "OK", "Warning", "Critical" y "Maintenance". Si cada badge crea su propio objeto de estilo, el frontend termina generando cientos o miles de instancias idénticas. Con Flyweight, centralizamos esos estilos compartidos en una fábrica que usa getOrCreate.
type BadgeVariant = "ok" | "warning" | "critical" | "maintenance";
class BadgeStyleFlyweight {
constructor(
public readonly variant: BadgeVariant,
public readonly background: string,
public readonly textColor: string,
public readonly icon: string
) {}
}
class BadgeStyleFactory {
private static cache = new Map<BadgeVariant, BadgeStyleFlyweight>();
static getOrCreate(variant: BadgeVariant): BadgeStyleFlyweight {
const existing = this.cache.get(variant);
if (existing) return existing;
const created = this.buildStyle(variant);
this.cache.set(variant, created);
return created;
}
private static buildStyle(variant: BadgeVariant): BadgeStyleFlyweight {
switch (variant) {
case "ok":
return new BadgeStyleFlyweight("ok", "#DCFCE7", "#166534", "check-circle");
case "warning":
return new BadgeStyleFlyweight("warning", "#FEF3C7", "#92400E", "alert-triangle");
case "critical":
return new BadgeStyleFlyweight("critical", "#FEE2E2", "#991B1B", "x-circle");
case "maintenance":
return new BadgeStyleFlyweight("maintenance", "#DBEAFE", "#1E3A8A", "tool");
}
}
static cacheSize(): number {
return this.cache.size;
}
}
class StatusBadge {
constructor(
private readonly label: string,
private readonly x: number,
private readonly y: number,
private readonly style: BadgeStyleFlyweight
) {}
render(): string {
return `[${this.label}] pos(${this.x},${this.y}) bg=${this.style.background} color=${this.style.textColor} icon=${this.style.icon}`;
}
}
const badges = [
new StatusBadge("API Gateway", 10, 20, BadgeStyleFactory.getOrCreate("ok")),
new StatusBadge("Payments", 10, 60, BadgeStyleFactory.getOrCreate("warning")),
new StatusBadge("Auth", 10, 100, BadgeStyleFactory.getOrCreate("critical")),
new StatusBadge("Reports", 10, 140, BadgeStyleFactory.getOrCreate("ok")),
new StatusBadge("Backups", 10, 180, BadgeStyleFactory.getOrCreate("maintenance")),
new StatusBadge("Search", 10, 220, BadgeStyleFactory.getOrCreate("ok"))
];
for (const badge of badges) {
console.log(badge.render());
}
console.log("Estilos únicos en caché:", BadgeStyleFactory.cacheSize());En este ejemplo, cada badge tiene estado extrínseco propio, como la etiqueta y la posición en pantalla. Pero el estilo visual se comparte. Si hay diez mil badges y solo cuatro variantes visuales, el ahorro puede ser significativo. Aquí no estamos compartiendo el componente completo, sino solo la parte común y reutilizable.
Qué gana este enfoque en el frontend
- Menos objetos repetidos en memoria.
- Mayor consistencia visual porque el estilo sale de una sola fuente.
- Más facilidad para medir cuántas variantes reales existen.
- Menor costo de recreación cuando el dashboard se actualiza con frecuencia.
Ejemplo realista en Java: plantillas de notificación reutilizables
Ahora pensemos en un backend Java que envía notificaciones por email y push. Muchos eventos distintos usan la misma plantilla base: bienvenida, recuperación de contraseña, factura emitida o alerta de seguridad. Sin Flyweight, cada envío podría reconstruir la plantilla completa. Con una fábrica getOrCreate, la plantilla compartida se crea una sola vez por tipo e idioma.
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
final class NotificationTemplateFlyweight {
private final String type;
private final String locale;
private final String subject;
private final String bodyTemplate;
public NotificationTemplateFlyweight(String type, String locale, String subject, String bodyTemplate) {
this.type = type;
this.locale = locale;
this.subject = subject;
this.bodyTemplate = bodyTemplate;
}
public String renderSubject() {
return subject;
}
public String renderBody(Map<String, String> variables) {
String result = bodyTemplate;
for (Map.Entry<String, String> entry : variables.entrySet()) {
result = result.replace("{{" + entry.getKey() + "}}", entry.getValue());
}
return result;
}
public String getType() {
return type;
}
public String getLocale() {
return locale;
}
}
final class NotificationTemplateFactory {
private static final Map<String, NotificationTemplateFlyweight> CACHE = new ConcurrentHashMap<>();
public static NotificationTemplateFlyweight getOrCreate(String type, String locale) {
String key = type + "::" + locale;
return CACHE.computeIfAbsent(key, k -> buildTemplate(type, locale));
}
private static NotificationTemplateFlyweight buildTemplate(String type, String locale) {
if (Objects.equals(type, "password_reset") && Objects.equals(locale, "es")) {
return new NotificationTemplateFlyweight(
type,
locale,
"Restablece tu contraseña",
"Hola {{name}}, usa este código: {{code}}. Expira en {{minutes}} minutos."
);
}
if (Objects.equals(type, "invoice_created") && Objects.equals(locale, "es")) {
return new NotificationTemplateFlyweight(
type,
locale,
"Tu factura está lista",
"Hola {{name}}, la factura {{invoiceNumber}} ya fue emitida por un total de {{amount}}."
);
}
return new NotificationTemplateFlyweight(
type,
locale,
"Notificación",
"Hola {{name}}, tienes una nueva notificación."
);
}
public static int cacheSize() {
return CACHE.size();
}
}
class NotificationSender {
public void sendPasswordReset(String name, String code, String minutes, String locale) {
NotificationTemplateFlyweight template = NotificationTemplateFactory.getOrCreate("password_reset", locale);
Map<String, String> variables = Map.of(
"name", name,
"code", code,
"minutes", minutes
);
System.out.println("SUBJECT: " + template.renderSubject());
System.out.println("BODY: " + template.renderBody(variables));
}
}
public class Main {
public static void main(String[] args) {
NotificationSender sender = new NotificationSender();
sender.sendPasswordReset("Ana", "834921", "10", "es");
sender.sendPasswordReset("Luis", "552188", "10", "es");
sender.sendPasswordReset("Marta", "991200", "10", "es");
System.out.println("Templates únicos en caché: " + NotificationTemplateFactory.cacheSize());
}
}Aquí la plantilla compartida contiene el asunto y el cuerpo base, mientras que los datos variables como nombre, código o importe se inyectan al momento de renderizar. Es un caso muy común en sistemas SaaS, ecommerce, fintech o plataformas educativas, donde una misma estructura de mensaje se reutiliza miles de veces.
Cómo implementar Flyweight sin complicarte de más
Una forma práctica de introducir Flyweight es empezar por un inventario de objetos repetidos. Si descubres que tu código crea constantemente instancias con los mismos atributos base, puedes mover esa parte a una fábrica. Luego, define una clave estable para el cache. Esa clave debe representar exactamente la combinación de atributos compartidos. Finalmente, asegúrate de que los consumidores entiendan qué datos vienen del flyweight y cuáles deben pasar como contexto externo.
- Busca clases o estructuras que se crean masivamente.
- Mide qué atributos se repiten en la mayoría de las instancias.
- Diseña una clave única: por ejemplo tipo, variante, idioma o categoría.
- Implementa getOrCreate con Map, ConcurrentHashMap o estructura equivalente.
- Haz inmutable el objeto compartido siempre que sea posible.
- Pasa el estado variable por argumentos, no lo guardes dentro del flyweight.
Preguntas frecuentes sobre Flyweight
¿Flyweight es lo mismo que un singleton?
No. Un singleton asegura una sola instancia global de una clase. Flyweight, en cambio, administra muchas instancias compartidas, normalmente indexadas por clave. Puedes tener un flyweight para cada variante de estilo, idioma o tipo de plantilla. Es decir, no hay una sola instancia total, sino un conjunto de instancias reutilizadas.
¿Qué diferencia hay entre caché y Flyweight?
Se parecen, pero no son idénticos. Una caché suele almacenar resultados para acelerar lecturas o cálculos. Flyweight se enfoca en compartir objetos para evitar duplicación de memoria. En la práctica, Flyweight suele apoyarse en una caché interna, pero su objetivo de diseño es estructural: separar estado compartido de estado variable.
¿Cuándo no debería usar Flyweight?
No conviene usarlo cuando los objetos son pocos, cuando casi no comparten datos, cuando el estado interno necesita mutar constantemente o cuando la legibilidad del código se degrada demasiado. Si el ahorro es marginal, una solución más simple suele ser mejor.
¿Cómo sé cuál es el estado extrínseco?
Hazte esta pregunta: ¿este dato cambia según el uso concreto del objeto? Si la respuesta es sí, probablemente sea extrínseco. Posición, usuario actual, cantidad, fecha y datos temporales suelen pertenecer a esa categoría.
Errores comunes al aplicar este patrón
- Guardar estado mutable dentro del objeto compartido.
- Usar claves ambiguas que devuelven instancias incorrectas.
- Aplicarlo sin haber detectado repetición real.
- Acoplar demasiado la fábrica con lógica de negocio.
- Olvidar el impacto en concurrencia en aplicaciones multihilo.
Conclusión
Flyweight es un patrón de diseño estructural muy útil cuando necesitas escalar sin desperdiciar memoria ni recrear objetos idénticos una y otra vez. Su fuerza está en una idea simple: compartir lo común, externalizar lo variable y resolver la reutilización con una fábrica getOrCreate. Si trabajas con interfaces con muchos elementos repetidos o con backends que reconstruyen plantillas, configuraciones o metadatos similares, este patrón puede darte una mejora tangible con una implementación relativamente sencilla. Bien aplicado, Flyweight no solo optimiza recursos: también te obliga a modelar mejor tu dominio.