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

Comparativa máquina/operario en Airames

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:

  1. La oficina técnica inicia el proceso en un libro de Excel.
  2. El operario en el taller recibe una hoja de fabricación en papel y anota a mano tiempos, paradas y partes.
  3. 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.