La innovación, nuestra razón de ser
Cuando diseñamos la arquitectura de un sistema, debemos ser extremadamente cautos con su complejidad y como el sistema se mantendrá a futuro. Siempre deberíamos tener en la cabeza que el mantenimiento no es tanto entender como funciona el sistema, o como atender a los usuarios finales, sino que, fundamentalmente, se relaciona con añadir nuevas funcionalidades que complemente o, incluso, modifiquen el comportamiento previamente diseñado.
De esta forma, nuestro foco debería estar en buscar diferentes aproximaciones que nos permitieran conseguir este objetivo. Siendo la principal como abordar la modularidad y la intercomunicación entre los distintos subsistemas involucrados. Aunque los sistemas puedan ser construidos de forma iterativa, no deberíamos despreciar, ni ignorar, una adecuada definición de la arquitectura que, lógicamente, estará basada en nuestro conocimiento y experiencia previa.
En este post me voy a enfocar en un ejemplo en el que hemos trabajado recientemente, en como hemos implementado una arquitectura modular, teniendo en mente desde el principio no los detalles de la implementación ni el camino completo que queríamos seguir, pero si el objetivo que queríamos lograr y, más o menos, como lo queríamos construir.
La verdad es que el tipo de aplicación que os voy a contar no es de las, aparentemente, más atractivas. Una aplicación de gestión, pero con ciertas peculiaridades relacionadas con la forma en la que los usuarios finales pueden interactuar con el sistema. De hecho, la aplicación, aunque orientada a usuarios internos también permitía la interacción con el usuario final.
Esta interacción consistía en distintos canales de entrada y salida, como, por ejemplo, correo electrónico, WhatsApp, Telegram, un cliente Web o una APP. Estos requerimientos son, así, a bote pronto, difusos… Podríamos platearnos crear un monolito que, pensaríamos, podría cubrir las necesidades del cliente. Pero si lo analizamos con cuidado nos encontraremos con una serie de aspectos importantes a abordar.
Contestar a estas preguntas, requiere que diseñemos el sistema con cuidado, en concreto:
Más allá de esta visión funcional, deberíamos tener en cuenta una serie de aspectos técnicos y operativos, ¿cómo conseguir una independencia real de varios equipos de desarrollo en paralelo? Y, a la vez, como conseguir no reinventar la rueda una y otra vez.
En la siguiente imagen os muestro la visión global de la arquitectura del sistema, de hecho bastante esquemática, pero espero que suficiente para tener una mejor comprensión de lo que estoy hablando.
Como podéis ver, he representado distintos módulos, o subsistemas, que se orientan a diferentes tareas, que espero os den una orientación de como hemos logrado tanto una arquitectura funcional como técnica adecuada.
En los siguientes apartados intentare daros una explicación más detallada de lo que estoy hablando.
La idea detrás de este API común es proporcionar a los diferentes agentes un sistema común de entrada/salida que evite que esto tengan acceso al sistema completo. Misión de este API sería la identificación, no de los usuarios, sino del agente y la posibilidad de que los agentes sean desplegados en distintos servidores.
Este sistema nos proporciona, por tanto, la entrada/salida y la seguridad a nivel de agente. Teniendo en cuenta que:
Aunque pueda parecer obvio, ni el cliente Web ni la App deberían almacenar tokens de seguridad para intercambiar información con el servidor, las implicaciones de seguridad son elevadas así que nunca hagáis eso. Entonces, ¿quién lo tiene? La respuesta, debemos crear un middleware que será responsable no sólo de almacenar los tokens sino de identificar los usuarios.
Más allá de la responsabilidad de almacenar los tokens e intercambiar la seguridad, tenemos también
No se debe reinventar la rueda. Los patrones de diseño son importantísimos cuando planificamos una arquitectura adecuada. Relacionado con los BOTS, tenemos en realidad un sistema común y compartido y, en el momento actual, dos implementaciones distintas.
Como podéis ver, los sistemas involucrados tratan de generalizar lo más posible, en concreto:
Si os dais cuenta, de lo que hablo, en realidad, es de típicos patrones de ingeniería de software. Desacoplamiento, aislamiento, responsabilidad única, …
Me resulta difícil imaginar un sistema que no tenga necesidad de eventos. Tu sistema dispara eventos cuando algo ocurre, y hay otros sistemas (subsistemas) que se suscriben a dichos eventos para hacer algo. No hay ni magia ni necesitas una compleja cola Kafka para soportar eventos. Un simple patron productor-subscriptor
Pese a la simplicidad, si lo que queremos es soportar un sistema auténticamente modular, necesitamos que ciertos aspectos complejos funcionen. Por ejemplo:
En resumen, cuando tengáis que plantear un sistema probablemente tengáis que utilizar eventos, y eso no implica una cola Kafka, eso es, en realidad, un detalle de implementación. Si diseñáis una arquitectura adecuada, deberías poder reemplazar este elemento por cualquier otra alternativa que se pudiera necesitar en el futuro.
Esto ya lo he comentado en posts anteriores, quizá uno de los problemas más destacados cuando desarrolláis una aplicación compleja es que un flujo de trabajo típico no es suficiente. No deberíamos terminar con un monolito en frontend, puesto que son complejos de mantener, complejos de desarrollar y complejos de probar.
La propuesta que os he comentado otras veces, dividir el frontend en una serie de micro-frontends cada uno de los cuales tenga, además, su capa de micro-backend y sus niveles de seguridad adecuados.
Tal y como os comenté antes, cuando os hablaba de las colas y la subscripción, soportar estos microfrontends requiere descubrimientos en caliente, classloaders personalizados y una configuración de construcción de la aplicación compleja que nos permita empaquetar todo el código en un único archivo desplegable. Afortunadamente, no tenéis que hacerlo más que una vez aplicando, por ejemplo, una adecuada configuración multi-proyecto en Gradle.
Más allá del empaquetamiento, hay otra serie de aspectos a considerar,
No necesitas crear arquitecturas extremadamente complejas, sólo usa aquello que necesites. Pero ten siempre en mente que no debes buscar la simplicidad por el mero hecho de que sea simple. Algunas cosas son complejas por naturaleza y, tu misión como ingeniero, es hacer que la complejidad sea operativa.