En Hibot Chat participé en la red de bots e integraciones que conecta cuatro canales de mensajería (WhatsApp, Facebook, Instagram y Telegram) con el núcleo de la plataforma. El sistema mueve más de 5.000 conversaciones al día y opera 24/7. Este registro explica cómo se estructura un backend así y por qué la arquitectura hexagonal es la que mejor le sienta.
Cuatro APIs, cuatro idiomas
Cada plataforma tiene su forma de entregar un mensaje: webhooks con formatos distintos, identificadores de usuario con reglas propias, adjuntos que llegan como URL en una y como referencia a descargar en otra, botones y plantillas que solo existen en algunas. Si esa diversidad llega al código que decide qué hacer con cada conversación, cada regla de negocio termina llena de condiciones por canal, y añadir uno nuevo obliga a tocarlo todo.
El núcleo no conoce los canales
En una arquitectura hexagonal el núcleo define lo que necesita mediante interfaces (los puertos) y cada tecnología externa se conecta por un adaptador. Aquí el núcleo de ruteo solo entiende un mensaje normalizado y una forma de responder. No importa el SDK de ninguna plataforma.
// Ilustrativo: el puerto de salida que el núcleo usa para responder.
// El núcleo no sabe si detrás hay WhatsApp o Telegram.
type Channel interface {
Send(ctx context.Context, to Recipient, msg Outgoing) error
}
// El mensaje que entra al núcleo, ya traducido por el adaptador del canal.
type Incoming struct {
Channel ChannelID // whatsapp, facebook, instagram, telegram
ConversationID string
From Recipient
Text string
Attachments []Attachment
ReceivedAt time.Time
}Cada canal tiene su adaptador de entrada, que recibe el webhook y lo traduce a Incoming, y su adaptador de salida, que implementa Channel con la API de esa plataforma. El núcleo recibe sus dependencias al arrancar y nunca importa un paquete de un canal concreto.
Normalizar sin perder información
Normalizar no es quedarse con el mínimo común. Lo que todos los canales comparten (texto, remitente, conversación, adjuntos) va en campos tipados. Lo que es propio de una plataforma se queda en el adaptador, que sabe cómo representarlo al responder. Así el núcleo trabaja con un modelo estable y cada canal conserva lo que lo hace distinto.
La ventaja se ve en las pruebas: las reglas de ruteo se prueban con mensajes normalizados y un Channel falso, sin levantar ninguna integración real.
Cuando el canal no responde
Cada plataforma impone límites de uso y, de vez en cuando, falla. En un diseño hexagonal esa responsabilidad vive en el adaptador, que es quien conoce las reglas de su canal: cuántas peticiones admite, qué errores son temporales y cuáles no. El núcleo pide enviar un mensaje; el adaptador decide cómo entregarlo sin saturar la plataforma, y la caída de un canal no arrastra a los otros tres.
Métricas de conversación
Con las conversaciones guardadas en MongoDB, las métricas salen de pipelines de agregación en la propia base: volumen por canal, conversaciones por día, tiempos de respuesta. Calcularlas en la base, en vez de traer los documentos a la aplicación, mantiene el cálculo cerca de los datos.
// Ilustrativo: conversaciones por canal y por día.
db.messages.aggregate([
{ $match: { receivedAt: { $gte: ISODate('2022-01-01') } } },
{ $group: {
_id: { channel: '$channel', day: { $dateTrunc: { date: '$receivedAt', unit: 'day' } } },
conversations: { $addToSet: '$conversationId' },
} },
{ $project: { conversations: { $size: '$conversations' } } },
{ $sort: { '_id.day': 1 } },
]);Lecciones
- Cuando varias integraciones hacen lo mismo de formas distintas, la diferencia debe quedarse en el borde del sistema, no en la lógica de negocio.
- Un modelo de mensaje normalizado hace que añadir un canal sea escribir un adaptador, no reescribir el núcleo.
- Los límites de uso y las caídas son reglas de cada canal: viven en su adaptador.
- Las reglas de ruteo se prueban sin integraciones reales si el núcleo solo depende de interfaces.