← Blog Arquitectura

Un Design System para 17 aplicaciones: cómo lo armé en Angular y lo que me tocó corregir

10 de octubre de 2026 · 6 min de lectura

Cuando una empresa tiene dos o tres aplicaciones internas, cada una con sus propios botones y sus propias tablas, nadie lo nota. Cuando llega a quince, el usuario sí lo nota: cada pantalla filtra distinto, cada tabla exporta distinto y el modo oscuro funciona en unas y en otras no.

Hoy mantengo un Design System en Angular que usan 17 aplicaciones de una misma plataforma. Cuento cómo está armado, qué decisiones tomé al inicio y cuáles me tocó corregir en el camino.

Cómo está armado

Es un monorepo con Nx y ocho librerías, cada una con una responsabilidad:

 theme        tokens de diseño y temas claro/oscuro
 shared       tipos, utilidades y pipes (sin dependencias de UI)
 icons        un solo componente de íconos
 core         servicios base: notificaciones, diálogos, carga, errores HTTP
 ui           componentes presentacionales: botón, tarjeta, chip, alerta…
 forms        controles de formulario
 data-table   la tabla, con paginación, búsqueda, acciones y exportación
 charts       gráficos

Se publican como paquetes privados en GitHub Packages, y cada aplicación los instala como cualquier otra dependencia. Separarlas así hace que una aplicación que solo necesita formularios no cargue los gráficos, y que Nx sepa exactamente qué reconstruir cuando algo cambia.

Las aplicaciones nunca ven Angular Material

Los componentes están construidos sobre Angular Material, pero ninguna aplicación lo importa. Solo ven componentes propios, con un prefijo de la casa (aquí los muestro como ds-button, ds-data-table) y servicios propios que envuelven a los de Material: el de notificaciones envuelve al MatSnackBar, el de diálogos al MatDialog.

Lo decidí así por dos motivos. El primero es poder actualizar Material, o cambiarlo, sin tocar diecisiete aplicaciones. El segundo es que, si una aplicación puede usar Material directamente, tarde o temprano alguien arma su propio botón "porque era más rápido" y ahí empieza otra vez el desorden.

Esta regla no depende de que la gente se acuerde. La hace cumplir el linter, con la regla de límites de módulos de Nx:

'@nx/enforce-module-boundaries': ['error', {
  depConstraints: [
    {
      sourceTag: 'material:forbidden',
      bannedExternalImports: ['@angular/material', '@angular/material/*']
    }
  ]
}]

Si una librería que no debe importar Material lo importa, el lint falla y el paquete no se publica.

Tokens en dos niveles

Los colores viven en dos capas. Los primitivos son los valores de la marca (el rojo, los grises). Los semánticos son variables CSS con nombre de uso: --ds-color-surface, --ds-color-text, --ds-color-border. Los componentes solo usan los semánticos, nunca un hexadecimal.

Con eso, el modo oscuro es cambiar el juego de valores de esas variables. No se recompila nada y el cambio es inmediato:

:root {
  --ds-color-surface: #ffffff;
  --ds-color-text: #1f2937;
}
@media (prefers-color-scheme: dark) {
  :root:not(.ds-light) {
    --ds-color-surface: #1e1e24;
    --ds-color-text: #e5e7eb;
  }
}

Aprendí por las malas por qué la regla de "solo tokens" también aplica a las aplicaciones, no solo a las librerías. Un formulario público, de los que se abren desde el celular, tenía la tarjeta y los textos con colores fijos. Los controles del Design System sí seguían el modo oscuro del teléfono, la tarjeta no. Resultado: texto gris claro sobre fondo blanco, ilegible. En la laptop de desarrollo, en modo claro, se veía perfecto. Desde entonces toda página pública se revisa con el sistema en modo oscuro antes de darla por buena.

Una tabla igual en todas las aplicaciones

El componente que más valor dio fue la tabla. ds-data-table trae paginación, orden, búsqueda, selección, acciones por fila, exportación a Excel y los estados de carga y de lista vacía. Cada aplicación le pasa los datos y las columnas, y nada más.

Pero tener el componente no alcanzó. Al principio cada módulo armaba su página de listado a su manera: uno con el botón "Nuevo" arriba a la derecha, otro con los filtros en una barra lateral, otro con un buscador que pedía elegir el campo. Así que escribí el estándar de cómo es una página de listado: la tabla siempre es ds-data-table; crear y filtrar van en el mismo menú, a la derecha del título; y hay un solo buscador sobre la tabla, que busca en todos los campos a la vez. Y dejé un módulo como referencia para copiar.

Desde que eso quedó escrito, una pantalla nueva sale igual a las demás aunque la haga otra persona del equipo.

Lo que me tocó corregir

Un servicio global dentro de un microfrontend

Las 17 aplicaciones se montan dentro de un portal con microfrontends (lo conté en este artículo). El servicio de diálogos del Design System se registra en la raíz (providedIn: 'root') y abre los diálogos sin un inyector propio. Dentro del portal, la raíz es la del portal, no la del módulo.

Así que un diálogo que hacía una llamada HTTP usaba el cliente HTTP del portal, sin el interceptor del módulo que agrega el token. El backend respondía 401 y el usuario veía "Sesión expirada" con la sesión perfectamente activa. Corriendo el módulo solo, en su propio puerto, funcionaba bien, porque ahí la raíz sí es la del módulo.

La solución inmediata fue abrir esos diálogos con MatDialog y el viewContainerRef del componente, y volver a proveer su servicio en las rutas del módulo. La lección más general: en un Design System que se va a usar dentro de microfrontends, cualquier servicio en la raíz tiene que pensarse para correr en un inyector que no es el tuyo.

Publicar sin romper

Al principio las ocho librerías subían de versión juntas, aunque solo cambiara una. Era simple, pero cada corrección en la tabla obligaba a publicar las ocho y a actualizar las ocho en cada aplicación. Hoy cada librería lleva su propia versión. El cuidado está en las dependencias internas: si ui pide core ^0.5.0 y core pasa a 0.6.0, ese rango ya no la acepta, así que hay que subirlas juntas.

La otra trampa fue el lockfile. El repositorio se desarrollaba en Windows y el pipeline de publicación corre en Linux, y npm resuelve distinto algunas dependencias opcionales de Storybook según la plataforma. Un package-lock.json generado en Windows hacía fallar el npm ci del pipeline. Lo resolví usando npm install en ese paso y dejándolo escrito en la guía de publicación, con el motivo, para que nadie lo "arregle" volviendo a npm ci.

Lo que se construye "mientras tanto"

Más de una vez alguien necesitó un componente, no lo encontró en la versión instalada y lo armó dentro de su módulo. A veces el componente ya existía en el código del Design System, solo que todavía no estaba publicado. La regla quedó así: antes de crear un componente se revisa el repositorio del Design System, no solo lo que está instalado. Y si de verdad falta, se agrega al Design System, no al módulo.

Qué haría distinto si empezara hoy

  • Pruebas desde la primera librería. Las librerías base arrancaron sin pruebas unitarias, y el pipeline tiene que dejar pasar ese caso. Agregar pruebas después, con 17 aplicaciones encima, cuesta mucho más.
  • Automatizar la actualización en las aplicaciones. Publicar es un comando. Lo caro es subir la versión y reinstalar en cada aplicación, y eso debería hacerlo un script o un bot que abra los cambios en cada repositorio.
  • El mismo Node en desarrollo y en el pipeline. La mitad de los problemas con el lockfile venían de desarrollar con una versión de Node y publicar con otra.
  • El catálogo publicado desde el día uno. El Storybook ayuda mucho más cuando cualquiera lo puede abrir en el navegador sin clonar nada. Es el primer lugar donde alguien debería buscar antes de construir.

Si estás por armar uno

Empieza con pocas piezas: los tokens, el botón, los controles de formulario y la tabla. Con eso se cubre la mayor parte de las pantallas de una aplicación empresarial. Y escribe desde el principio las reglas de cómo se usa, porque el componente solo no alcanza para que todas las pantallas se parezcan.

Si mantienes un Design System para varios equipos, escríbeme por LinkedIn. Me interesa saber cómo resuelven la actualización en todas las aplicaciones.

Design SystemAngularNxAngular MaterialFrontendMicrofrontends