Por qué diseñamos nuestro MES (Airames) con arquitectura hexagonal

Cuando digitalizamos fábricas en Helinx, el objetivo casi nunca es “crear una aplicación web desde cero”. El reto real es eliminar la ineficiencia operativa.
En nuestro sistema MES (Airames), desplegado en varios entornos industriales, el flujo de trabajo clásico era papel y hojas de cálculo:
- La oficina técnica inicia el proceso en un libro de Excel.
- El operario en el taller recibe una hoja de fabricación en papel y anota a mano tiempos, paradas y partes.
- El administrativo vuelve a picar los datos del papel al Excel para cerrar el seguimiento.
A esto se sumaba la falta de datos objetivos de máquina: el tiempo teórico del operario rara vez coincidía con la realidad. Para corregirlo, conectamos las máquinas y medimos automáticamente dos variables críticas: si la máquina está encendida/activa y la cantidad de piezas reales fabricadas.
Para construir un sistema capaz de digerir ese proceso sin volverse inmanejable con el tiempo, tomamos la decisión de diseño que más pesa en el proyecto: arquitectura hexagonal.
El problema no era “hacer pantallas”
Si te limitas a poner formularios web encima de un proceso caótico, lo único que consigues es trasladar el caos de la papelera al navegador.
Un entorno low-code o una web acoplada a la base de datos habría sido más rápido para un formulario inicial, pero no habría aguantado las reglas de la fábrica ni los cambios de ERP. Hacía falta escribir esas reglas en un sitio que no fuera Next.js ni Prisma.
Lo comparo con más calma en Power Apps vs desarrollo web.
Qué es el “dominio” (sin jerga)
Dominio = las reglas de la fábrica, no el software.
Es lo que un jefe de producción entiende sin hablar de frameworks:
- Qué es una orden y cuándo se puede cerrar
- Qué es un fichaje válido
- Cuándo el parte del operario choca con las piezas contadas por la máquina
- Qué cuenta un turno (día / noche / 24 h)
Eso existía antes del MES: en el papel, en la cabeza del encargado, en el Excel. El dominio es ese conocimiento, pasado a código.
| No es dominio | Sí es dominio |
|---|---|
| Next.js, Prisma, PostgreSQL | “No puedes cerrar si la máquina no ha contado esas piezas” |
| ThingsBoard, Odoo, Power BI | “Una orden abierta no se borra: se cancela” |
Un route.ts |
“Tiempo teórico del operario vs tiempo real de máquina” |
Hexagonal, en una frase: esas reglas no viven dentro de Prisma ni de la pantalla.
El dominio en el centro
Regla estricta: el framework no sabe qué es una orden de fabricación.
Next.js y Prisma son periféricos. En el núcleo solo hay casos de uso de producción:
- Abrir, pausar y cerrar órdenes.
- Reportar piezas e incidencias.
- Contrastar operario vs máquina (piezas y estado ON/OFF).
- Consolidar por turno (día, noche, 24 h).
Si mañana la UI de taller pasa a una tablet industrial, “cómo se cierra un turno” no se reescribe.
La diferencia real: Next.js acoplado vs hexagonal
En muchos proyectos Next.js la lógica acaba prisionera de la ruta y del ORM: Server Actions o app/api/... importan Prisma y mezclan HTTP, SQL y reglas de fábrica. En producción eso duele en cuanto cambia la fuente de telemetría o el ERP.
1. Estructura habitual (lógica acoplada)
// app/api/ordenes/cerrar/route.ts
import { prisma } from '@/lib/prisma';
export async function POST(req: Request) {
const { ordenId, piezasOperario } = await req.json();
// Negocio atrapado en el endpoint y en Prisma
const maquina = await prisma.telemetria.findFirst({ where: { ordenId } });
if (piezasOperario > maquina.piezasContadas) {
throw new Error('Desfase de piezas');
}
return Response.json(
await prisma.orden.update({
where: { id: ordenId },
data: { estado: 'cerrada', piezasOperario }
})
);
}
Si mañana las piezas ya no salen de PostgreSQL sino de MQTT / ThingsBoard, o cambias Prisma por otro ORM, reescribes la API, los tests y la lógica del cierre de turno.
2. Cómo lo estructuramos en Airames
Separación clara: dominio puro, casos de uso, adaptadores. La estructura de ficheros lo deja a la vista:
src/
├── domain/ ← Núcleo. Sin Next.js. Sin Prisma.
│ ├── entities/
│ │ ├── Orden.ts
│ │ ├── Fichaje.ts
│ │ └── TelemetriaMaquina.ts
│ └── ports/
│ ├── OrdenesRepositoryPort.ts ← Interface: obtener, guardar
│ └── IoTDataPort.ts ← Interface: piezas, estado ON/OFF
│
├── application/ ← Casos de uso (orquestan el dominio)
│ └── use-cases/
│ ├── AbrirOrdenUseCase.ts
│ └── CerrarTurnoUseCase.ts
│
└── infrastructure/ ← Adaptadores hacia el exterior
├── persistence/
│ └── PrismaOrdenesRepository.ts
├── iot/
│ └── ThingsBoardIoTAdapter.ts
└── http/
└── ordenes/
└── cerrar/
└── route.ts ← Next.js: solo HTTP → use case
Dependencias hacia dentro (la flecha no se invierte):
[ Next.js / Prisma / ThingsBoard ] ← infrastructure
│
▼ implementan ports
[ CerrarTurnoUseCase ] ← application
│
▼ usa entidades + ports
[ Orden · Fichaje · ports ] ← domain (no conoce el exterior)
El caso de uso recibe puertos, no librerías concretas:
// application/use-cases/CerrarTurnoUseCase.ts
export class CerrarTurnoUseCase {
constructor(
private ordenRepo: OrdenesRepositoryPort,
private iotService: IoTDataPort
) {}
async execute(ordenId: string, piezasOperario: number) {
const orden = await this.ordenRepo.obtenerPorId(ordenId);
const piezasRealMaquina = await this.iotService.obtenerPiezas(orden.maquinaId);
// Regla de negocio pura: mockeas los ports y el test no toca BBDD ni HTTP
orden.validarCierre(piezasOperario, piezasRealMaquina);
await this.ordenRepo.guardar(orden);
}
}
El route.ts o Server Action de Next.js sigue existiendo: solo instancia el use case, traduce HTTP y devuelve la respuesta. Next.js es capa de entrada / UI, no el dueño de la lógica de la fábrica.
Adaptadores y puertos
Alrededor del dominio definimos puertos (interfaces) que se conectan al exterior con adaptadores:
| Puerto | Adaptador actual | Razón de la abstracción |
|---|---|---|
| Interfaz de usuario | Next.js / React | Taller (operario) y oficina (gestión) no son el mismo flujo |
| Persistencia | Prisma + PostgreSQL | La base de datos es detalle; el SQL no se mezcla con las reglas |
| Sistemas legados | Odoo / Excel / CSV | Si el ERP o el Excel cambian, solo cambia el adaptador |
| Captura de máquina | Ingesta IoT (estado / piezas) | ON/OFF y conteo: la máquina aporta la verdad física; el operario la contextualiza |
| Reporting | Power BI | Los cuadros leen lo consolidado sin interferir en la transacción de producción |
La captura de la imagen —comparativa máquina/operario— es regla de negocio, no un componente de tabla ni una query de Grafana. El lado “cómo está la máquina” en campo lo desarrollo en monitorización IoT y en ThingsBoard vs Grafana.
Beneficios en la práctica (antes / después)
Abstracto (“si cambia el ERP…”) no convence. Tres escenarios reales de Airames:
1. Cambia la fuente de piezas de máquina
Sin hexagonal: la regla piezasOperario vs piezasMáquina está en el route.ts con Prisma. Pasas de PostgreSQL a MQTT / ThingsBoard → reescribes API, tests y la lógica del cierre.
Con hexagonal: solo tocas el adaptador (ThingsBoardIoTAdapter o el de MQTT). CerrarTurnoUseCase y validarCierre siguen igual.
2. Nueva pantalla en taller (tablet)
Sin hexagonal: suele aparecer otro endpoint o Server Action que copia la misma validación. Dos sitios que mantener; uno se desactualiza.
Con hexagonal: la tablet llama al mismo CerrarTurnoUseCase. Cero duplicar la regla del desfase.
3. Odoo deja de ser el origen de las órdenes
Sin hexagonal: el cierre de turno importa tablas o clientes de Odoo. Cambiar de ERP = reescribir el caso de uso.
Con hexagonal: cambias el adaptador que trae/guarda la orden. El dominio sigue definiendo qué es cerrar bien.
4. Tests en CI (el que más nota un desarrollador)
Sin hexagonal: para probar el desfase necesitas base de datos o mocks gordos de Prisma.
Con hexagonal: mockeas OrdenesRepositoryPort e IoTDataPort → el test de validarCierre corre en milisegundos, sin HTTP ni BBDD.
Lo que gana la fábrica
Un MES vive más que cualquier framework. Cambian máquinas, turnos, sensores y, tarde o temprano, el ERP.
Hexagonal no es dogma: es que la operativa no dependa del software de esta semana. Next.js, Prisma y PostgreSQL son el vehículo. La carga útil es la producción —el dominio.
Si estás digitalizando una fábrica o pelearías este diseño en tu entorno, escríbeme o sígueme en LinkedIn.