learning-front

Nivel 8 · Calidad: que no se rompa en producción

Arquitectura: DDD, hexagonal y patrones

Organizar el código por dominio, reconocer antipatrones y los patrones útiles (Observer, Strategy y el Repository para aislar el acceso a datos). Micro frontends, de concepto.

Hasta ahora has escrito piezas: componentes, hooks, funciones, sus tests. Cuando un proyecto crece, aparece una pregunta nueva y sorprendentemente difícil: ¿dónde va cada cosa, y qué puede depender de qué? Eso es la arquitectura. No es ceremonia ni diagramas por gusto: es lo que permite que un código —y un equipo— crezcan sin colapsar. Este capítulo es un mapa de las ideas que más se usan en el frontend de 2026. Apóyate en lo que ya viste de clean code en el Nivel 4 (SOLID, y los patrones Factory, Singleton y Adapter): aquí seguimos por ahí.

Organizar por dominio, no por capa técnica#

La primera decisión es cómo agrupas los ficheros. Hay dos formas. Por capa técnica, un cajón por tipo de fichero:

text
src/
  components/   ← TODOS los componentes de la app
  hooks/        ← TODOS los hooks
  services/     ← TODOS los servicios
  types/        ← TODOS los tipos

Y por dominio (la idea detrás del DDD, Domain-Driven Design), una carpeta por funcionalidad del negocio, cada una con lo suyo dentro:

text
src/
  equipos/      ← la feature "equipos", entera
    EquipoCard.tsx
    useEquipo.ts
    equipo.service.ts
    equipo.types.ts
  heroes/       ← la feature "héroes", entera
    HeroeCard.tsx
    useHeroes.ts
    heroe.repository.ts
    heroe.types.ts
  shared/       ← lo de verdad común (Botón, utilidades)

¿Y qué más da, si los ficheros son los mismos? Da mucho cuando tocas algo. Con la organización por capa, cambiar la feature “héroes” te obliga a saltar entre cuatro carpetas distintas y a bucear entre los ficheros de TODAS las features para encontrar los de héroes. Con la organización por dominio, abres heroes/ y está todo junto: el cambio queda contenido, y alguien nuevo encuentra las cosas por lo que hacen, no por su extensión. A escala, esto es la diferencia entre un código navegable y uno en el que nadie sabe dónde vive nada.

Arquitectura hexagonal: el dominio en el centro#

La segunda idea es sobre dependencias: quién conoce a quién. En una arquitectura hexagonal (también llamada puertos y adaptadores), el núcleo de dominio —tu lógica de negocio— no conoce el mundo exterior: ni React, ni la API, ni la base de datos. Habla con ellos solo a través de puertos (interfaces), y unos adaptadores implementan esos puertos.

text
   ┌─────────────── adaptadores (el exterior) ───────────────┐
   │   React (UI)        API REST          localStorage       │
   └──────┬───────────────────┬──────────────────┬───────────┘
          │ implementa        │ implementa        │ implementa
       ┌──▼───────────────────▼──────────────────▼──┐
       │            PUERTOS (interfaces)             │
       │     ┌────────────────────────────────┐     │
       │     │   NÚCLEO DE DOMINIO (la lógica) │     │
       │     │   no sabe nada del exterior     │     │
       │     └────────────────────────────────┘     │
       └────────────────────────────────────────────┘

Las dependencias apuntan hacia adentro: el exterior conoce al dominio, nunca al revés. ¿La consecuencia? El dominio se puede probar y reutilizar sin arrastrar la UI ni la red. Es la D de SOLID (depende de abstracciones, no de detalles) llevada a la estructura de la app. Y el puerto más común es el del acceso a datos: el Repository.

Repository: aislar el acceso a datos#

El patrón Repository pone una interfaz entre tu lógica y el origen de los datos. El dominio depende del contrato (obtenerTodos, guardar…), no de fetch ni de la base de datos.

Mira mejorDelRol: recibe un HeroeRepository y no tiene ni idea de si los datos vienen de memoria, de una API o de un fichero. ¿Y qué ganas? Dos cosas concretas. Una, puedes cambiar el origen (de memoria a API real) sin tocar una línea de la lógica. Y dos —la que conecta con todo lo que acabas de aprender—, puedes testear mejorDelRol pasándole un repo en memoria con datos de ejemplo, sin red. Una dependencia detrás de una interfaz es una dependencia que puedes sustituir: eso es exactamente lo que hacía testeable el código en los capítulos anteriores.

Strategy: algoritmos intercambiables#

El patrón Strategy encapsula algoritmos intercambiables tras un tipo común. En lugar de un if gigante que crece con cada caso, cada algoritmo es una pieza, y una función las recibe y aplica.

Fíjate en que rankear no conoce ningún algoritmo: recibe la estrategia y la aplica. ¿Y qué gana eso? Que añadir una forma de ordenar nueva (alfabética, por victorias) es escribir una pieza nueva, no editar rankear. Eso es la O de SOLID: abierto a extensión, cerrado a modificación. Lo contrario —un if/else if creciente dentro de rankear— te obligaría a tocar código ya probado cada vez, con el riesgo de romperlo. Lo practicas tú en el ejercicio.

Observer: avisar a quien escucha#

El patrón Observer es un sujeto que avisa a quien se haya suscrito cuando algo cambia. El sujeto no sabe quién escucha: solo emite.

El emisor y los dos observadores están desacoplados: puedes añadir o quitar oyentes sin tocar el emisor. ¿Por qué te importa esto? Porque es la base conceptual de cosas que ya usas o usarás: los stores de estado (en Zustand te suscribes a una porción del estado y tu componente se re-renderiza cuando cambia), los buses de eventos, RxJS. Reconocer el patrón hace que esas herramientas dejen de ser magia: por dentro, son alguien emitiendo y alguien escuchando.

Antipatrones: lo que conviene reconocer#

Tan útil como los patrones es oler los antipatrones. El más común en React es el componente que lo hace todo: pide datos, guarda estado, contiene la lógica de negocio Y pinta, todo mezclado.

tsx
// Antipatrón: un componente que mezcla red, lógica y UI. Imposible de testear por partes.
function PanelHeroes() {
  const [heroes, setHeroes] = useState([]);
  useEffect(() => {
    fetch("/api/heroes")               // ← red
      .then((r) => r.json())
      .then((datos) =>
        // ← lógica de negocio metida en la UI
        setHeroes(datos.filter((h) => h.victorias / h.partidas > 0.5)),
      );
  }, []);
  return <ul>{heroes.map((h) => <li>{h.nombre}</li>)}</ul>; // ← presentación
}

¿Y qué tiene de malo, si funciona? Que no puedes probar la regla de negocio (el > 0.5) sin montar el componente y simular la red, y que el día que esa regla se use en otro sitio, la copiarás. La versión sana separa: un repository trae los datos, una función de dominio aplica la regla (testeable sola, sin red), y el componente solo pinta lo que recibe. Cada pieza, una responsabilidad. Otros antipatrones que reconocerás con el tiempo: pasar props por seis niveles (prop drilling) cuando tocaría un contexto o un store, y el Singleton como estado global disfrazado (que ya viste en el Nivel 4).

Micro frontends, de concepto#

Para cerrar, una idea que oirás pero que casi nunca es tu punto de partida: los micro frontends. La idea es dividir un frontend grande en piezas desplegables de forma independiente, cada una propiedad de un equipo distinto (el equipo de “pagos” despliega su trozo sin esperar al de “catálogo”).

El trade-off es honesto: ganas autonomía a escala —equipos que no se bloquean entre sí—, a cambio de complejidad (integrar las piezas, compartir estilos y versiones, posible duplicación). Es una solución para organizaciones grandes con muchos equipos; en un proyecto normal, parte de un monolito bien organizado por dominio (lo de arriba) y plantéate micro frontends solo si el tamaño del equipo lo pide de verdad.

Pruébalo#

Abre el playground de Strategy de arriba y añade una tercera estrategia —por ejemplo, alfabetico, que ordene por nombre con localeCompare— y úsala con la misma rankear. Comprueba que no tocas rankear ni las otras estrategias: solo añades. Esa sensación —crecer sin editar lo que ya funciona— es lo que persigue toda buena arquitectura.

Comprueba lo que sabes#

Pregunta 1 de 5

Organizar el código "por dominio" (DDD) en vez de "por capas técnicas" significa…

Tu turno#

Implementa el patrón Strategy para rankear héroes. Empieza con una estrategia y rankear; luego añade más sin tocar rankear. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en el salto de un tier al siguiente: de una estrategia a varias, y de varias a estrategias puras que crecen sin riesgo.

Ejercicio · en esta página

Implementa el patrón Strategy para rankear

Monta el patrón Strategy para ordenar héroes del Team Builder. Define el tipo EstrategiaRanking (una función que ordena héroes), implementa al menos porWinrate y una función rankear(heroes, estrategia) que aplique la estrategia que recibe. La clave: rankear debe servir para CUALQUIER estrategia sin cambiar cuando añadas una nueva.

Paso 1: El patrón con una estrategia

  • Defines el tipo EstrategiaRanking: una función (heroes: Heroe[]) => Heroe[].
  • Implementas porWinrate (mayor winrate primero) y rankear, que recibe la estrategia y la aplica.
  • rankear no contiene la lógica de ordenación: la delega en la estrategia.
Ver soluciones
// Tier OK: el patrón Strategy con una estrategia. rankear ya separa el QUÉ del CÓMO.
export interface Heroe {
  nombre: string;
  partidas: number;
  victorias: number;
}

// Una estrategia es una función que recibe héroes y los devuelve ordenados.
export type EstrategiaRanking = (heroes: Heroe[]) => Heroe[];

// Estrategia concreta: por winrate (victorias/partidas), de mayor a menor.
// [...heroes] copia antes de ordenar: sort muta, y no queremos tocar el original.
export const porWinrate: EstrategiaRanking = (heroes) =>
  [...heroes].sort((a, b) => b.victorias / b.partidas - a.victorias / a.partidas);

// rankear no sabe NADA del algoritmo: recibe la estrategia y la aplica.
export function rankear(
  heroes: Heroe[],
  estrategia: EstrategiaRanking,
): Heroe[] {
  return estrategia(heroes);
}

// Demo: el ranking por winrate.
const heroes: Heroe[] = [
  { nombre: "Genji", partidas: 20, victorias: 13 },
  { nombre: "Mercy", partidas: 30, victorias: 21 },
  { nombre: "Reinhardt", partidas: 12, victorias: 9 },
];
console.log(
  "Por winrate: " + rankear(heroes, porWinrate).map((h) => h.nombre).join(", "),
);

Por qué este nivel

  • Monta el patrón: una estrategia es una función, y rankear la recibe y la aplica. Ya separa el QUÉ (ordenar héroes) del CÓMO concreto (por winrate).
  • Su límite: con una sola estrategia no se ve la ventaja real. El patrón brilla cuando hay varias y se pueden intercambiar sin tocar rankear.