Introducción: Desafíos de los Monolitos y Ventajas de los Microservicios con Spring
En la primera parte discutimos los problemas inherentes a los sistemas monolitos y cómo los microservicios con spring proporcionas soluciones efectivas. Ahora, exploraremos en detalle cómo implementar y gestionar la comunicación y el almacenamiento de datos entre microservicios utilizando Java y Spring, destacando la modularidad y escalabilidad que esta arquitectura permite.
Desafíos de almacenar datos en microservicios con Spring
El primer gran desafío que viene con los microservicios es almacenar datos. Esto es más difícil de lo que piensas. Lo más importante es que no quieres tener solo una base de datos para todos los servicios. Así que evitaremos esto:
¿Por qué es problemático usar una sola base de datos para todo?
Utilizar una única base de datos para todos los componentes de una aplicación, como Productos, Pagos, Entregas y Órdenes, puede llevar a varios problemas:
Acoplamiento Estrecho: Cuando todos los servicios dependen de una base de datos central, cualquier cambio en la estructura de la base de datos puede tener un impacto desproporcionado. Por ejemplo, si el esquema de la tabla de productos cambia, podría potencialmente romper el servicio de órdenes que depende de esa tabla para su funcionamiento.
Falta de Flexibilidad: Con una base de datos compartida, realizar cambios o actualizaciones se vuelve riesgoso. Las modificaciones deben planificarse y ejecutarse con cuidado extremo para evitar interrupciones en otros servicios que también acceden a la misma base de datos.
Escalabilidad Complicada: A medida que la aplicación crece, escalar una base de datos monolítica puede convertirse en un cuello de botella, ya que todos los servicios aumentan su carga simultáneamente, pudiendo afectar el rendimiento general.
La Solución de Microservicios con Spring
Adoptar una arquitectura de microservicios significa que cada parte de la aplicación opera de manera independiente con su propia base de datos. Esto produce múltiples ventajas:
Desacoplamiento: Los servicios son autónomos y no están directamente ligados a los esquemas de los demás servicios. Por ejemplo, el servicio de Pagos puede evolucionar y cambiar su base de datos sin afectar a los demás servicios como Productos o Entregas.
Flexibilidad en Desarrollo y Mantenimiento: Cada equipo puede desarrollar, probar y desplegar actualizaciones en su servicio de manera independiente, lo que acelera los ciclos de desarrollo y reduce los riesgos asociados con las actualizaciones en producción.
Optimización Específica: Cada servicio puede utilizar la tecnología de base de datos que mejor se adapte a sus necesidades específicas. Por ejemplo, un servicio de Pagos podría beneficiarse de usar una base de datos SQL para manejar transacciones complejas, mientras que un servicio de Productos podría optar por una solución NoSQL para manejar grandes volúmenes de datos con estructuras menos rígidas.
ImplementaciónPráctica de Microservicios con Spring Framework
Ejemplo Práctico de Microservicios de Orders y Productos
A continuación, presentamos un ejemplo más práctico que ilustra cómo dos microservicios, el Servicio de Pedidos y el Servicio de Productos, pueden ser implementados y comunicarse entre sí utilizando Spring Boot y Spring Cloud.
1. Creación del Servicio de Productos:
El Servicio de Productos gestionará información sobre productos disponibles para ser comprados. A continuación, se muestra una configuración básica de este servicio en Spring Boot:Comunicación entre microservicios:
En este código, ProductService es una clase de servicio que se encargará de interactuar con la base de datos (no mostrada aquí por brevedad) para recuperar los detalles del producto.
2. Creación del Servicio de Pedidos
El Servicio de Pedidos será responsable de manejar la creación de pedidos que incluyen productos:
En este código, el OrderController utiliza RestTemplate para hacer llamadas HTTP al Servicio de Productos para validar los productos antes de crear un pedido. La URL http://product-service/products/ asume que estamos utilizando algún tipo de descubrimiento de servicios, como Eureka, para resolver el servicio real a partir de su nombre lógico.
El otro gran desafío que tenemos que resolver es la comunicación entre servicios. Por lo tanto, dividimos nuestra aplicación en diferentes servicios pequeños. Pero ya no podemos llamar a funciones entre dos servicios como lo hacíamos en un monolito. En otras palabras, ¿cómo obtiene el servicio de Órdenes los pedidos para un informe? Hay dos formas de resolver ese problema (de hecho, hay más de dos soluciones, pero nos centraremos en estas dos). Spring Framework ofrece varias opciones para facilitar esta comunicación, adaptándose a las necesidades de sincronización o asincronización según el caso de uso.
Comunicación sincrónica
Esta es la forma más sencilla de entender e implementar, por lo que empezaremos por aquí. Utilizamos un modelo de solicitud-respuesta para comunicarnos entre servicios. Aunque no nos limitamos exclusivamente a HTTP, este es el método más común (otras soluciones pueden ser RPC y protocol buffers).
Permíteme mostrarte un diagrama actualizado:
Cada flecha en el diagrama representa una solicitud API HTTP. Así es cómo funciona en orden:
El navegador envía una solicitud HTTP al servicio de Order y al servicio de Product.
Tanto Order como Product envían solicitudes al servicio de Payment y al servicio de Delivery.
Payment y Delivery procesan estas solicitudes y devuelven la respuesta necesaria.
Características de la Comunicación Sincrónica
Esto se conoce como comunicación sincrónica porque cada paso se espera a que el anterior haya completado:
Los servicios de Order y Product, tras recibir la solicitud inicial, deben completar sus propias operaciones y luego comunicarse con Payment y Delivery para proceder.
Después de que cada solicitud es procesada, Payment y Delivery devuelven los resultados necesarios para que Order y Product puedan finalizar sus procesos y responder al requerimiento inicial del Actor.
Ventajas y Desafíos de la Comunicación Sincrónica:
Ventajas:
Claridad en el proceso: Es fácil de entender, muy parecido a las llamadas de funciones en una arquitectura monolítica, aunque en forma de solicitudes HTTP.
Desacoplamiento de los servicios: Aunque los servicios se están comunicando, cada uno mantiene su independencia, facilitando su desarrollo y mantenimiento individual.
Desafíos:
Dependencia entre servicios. Esto es importante. Queremos usar microservicios porque podemos desarrollar servicios independientemente unos de otros, y podemos desplegarlos cuando queramos. Si los acoplamos con llamadas HTTP, solo complica las cosas.
Más fuentes de errores. Hacemos muchas solicitudes HTTP dentro de nuestros servicios. ¿Qué pasa si la solicitud de Order a Product falla? Entonces toda la cadena de solicitudes falla. ¿Qué pasa si un servicio está caído y ocurre un tiempo de espera? Entonces toda la cadena de solicitudes falla. Esto nos lleva a nuestra próxima desventaja.
Latencia. Hacemos muchas solicitudes. Imagina que tenemos solo 30 servicios. Podemos fácilmente hacer una cadena de solicitudes de 20-30 llamadas HTTP. Es simplemente lento, mucho más lento que 20-30 llamadas a funciones en un monolito.
Dependencias circulares. ¿Qué pasa si el servicio de Order llama al servicio de Product, y luego el servicio de Product llama al servicio de Delivery, pero por alguna razón, el servicio de Delivery también llama al servicio de Product?
Consideraciones Finales sobre la Comunicación Sincrónica
Aunque la comunicación sincrónica es directa y a menudo más fácil de implementar, presenta desafíos que pueden complicar la arquitectura de microservicios. Es vital considerar estos factores al diseñar sistemas que dependen de la inmediatez de la respuesta.
¿Entonces, qué otra alternativa tenemos?
Comunicación Basada en Eventos Utilizando Kafka
Introducción al Modelo de Eventos con Kafka
Kafka actúa como una plataforma de streaming distribuido. Kafka permite a los servicios publicar y suscribirse de flujos de eventos de manera eficiente y con alta disponibilidad, facilitando así la comunicación asincrónica entre los servicios.
Este modelo permite una comunicación asincrónica entre los servicios, mejorando la desacoplación y la escalabilidad horizontal.
Diagrama Ejemplo Práctico: Uso de Kafka en la creación de Órdenes
A continuación, se muestra cómo los servicios pueden integrar Kafka para manejar eventos de manera efectiva.
Creación de la Orden
Inicio del Proceso
Un actor (por ejemplo, un usuario) envía una solicitud para crear una nueva orden mediante un POST /orders.
El servicio de Order recibe esta solicitud HTTP POST con los datos de la orden.
Procesamiento y Almacenamiento
El servicio de Order procesa la solicitud, calcula lo necesario, y guarda los detalles de la nueva orden en su base de datos Orders DB.
Publicación del Evento
Una vez creada la orden, el servicio de Order publica un evento llamado OrderCreated en un tema específico de Kafka denominado Kafka Topic: Orders. Este evento incluye toda la información relevante sobre la orden, como el monto total, la mesa, el cliente, y el arreglo de productos.
Distribución del Evento
Kafka, actuando como intermediario, distribuye el evento OrderCreated a todos los servicios suscritos que estén interesados en este tipo de eventos.
Consumo y Procesamiento del Evento
Los servicios de Payment y Delivery están configurados para consumir eventos de este tipo desde el tema de Kafka.
Al recibir el evento, ambos servicios procesan la información relevante y realizan operaciones de inserción o actualización en sus respectivas bases de datos (Payment db y Delivery db).
Resultados y Beneficios:
Este modelo permite que cada servicio construya y mantenga su propia base de datos basada en los eventos que consume, lo que minimiza la necesidad de solicitar información directamente de otros servicios.
Por ejemplo, no es necesario que el servicio de Payment realice una solicitud HTTP directamente al servicio de Order para obtener los datos que necesita.
Nota Importante sobre el Uso de Kafka:
Utilizando Kafka como sistema de manejo de eventos, se mejora la eficiencia en la comunicación entre servicios al reducir la dependencia directa y al permitir un sistema más escalable y resiliente.
Flujo de Datos en el Servicio de Órdenes Utilizando Kafka
Creación de la Orden
Inicio de la solicitud:
Un actor, como un usuario o un sistema automatizado envía una solicitud POST al endpoint /orders para crear una nueva orden.
Procesamiento del Evento
El servicio de Ordenes procesa esta solicitud y guarda los detalles de la orden en la base de datos, específicamente en la tabla de órdenes. Este detalle almacenado es lo que llamamos el modelo interno de Orden, que solo es visible para el servicio de Ordenes y puede contener hasta 20 columnas.
Publicación del Evento
Generación del Evento OrderCreated
Una vez que la orden está completamente procesada y almacenada, el servicio de Órdenes publica un evento denominado OrderCreated en el tópico de Kafka llamado “Orders”. Este evento incluye detalles esenciales pero simplificados de la orden, conocidos como el “modelo compartido de la orden” o DTO. Esta representación contiene solo información crítica necesaria para otros servicios, garantizando que los detalles internos permanezcan privados y seguros.
Distribución y Consumo del Evento
Distribución Automática del Evento:
Kafka distribuye el evento OrderCreated a todos los servicios suscritos que están configurados para escuchar este tópico específico.
Procesamiento del Evento por Servicios Subscriptores:
Los servicios como Payment y Delivery consumen el evento. Utilizan la información del modelo compartido de la orden para actualizar sus propias bases de datos y realizar operaciones necesarias sin requerir una comunicación directa con el servicio de Órdenes.
Consideraciones Importantes
Seguridad y Privacidad de la Información:
El evento publicado por el servicio de Órdenes incluye solo el modelo compartido de la orden (DTO), asegurando que la información detallada y sensible no trascienda los límites del servicio que la genera. Esto mantiene la integridad y la confidencialidad de los datos internos.
La promesa de los microservicios
Espero que todo esto suene bien y muy arquitectónico, pero hay una pregunta más importante. ¿Por qué estamos haciendo esto?
Ventajas de los Microservicios
1. Modularidad y Escalabilidad
Base de Código Reducida: Todos nuestros servicios serán pequeños, fáciles de entender y simples. Esto permite una mayor manejabilidad y menor complejidad.
Resiliencia y Fiabilidad: Casi todos los errores y bugs tienen un alcance de un solo servicio. Esto significa que si algo sale mal en un servicio, no afecta a todo el sistema.
Despliegues Frecuentes: Con servicios más pequeños, es más fácil desplegar y lanzar nuevas características frecuentemente sin riesgos mayores.
Disponibilidad Mejorada: Utilizando herramientas como Docker Swarm o Kubernetes para orquestar los servicios, se mejora la disponibilidad y se facilita la escalabilidad.
2. Eficiencia de Desarrollo
Equipos Pequeños y Focalizados: Equipos de 4-5 miembros pueden desarrollar y mantener unos pocos servicios, facilitando la gestión del desarrollo y reduciendo la curva de aprendizaje.
Flexibilidad Tecnológica: Se puede utilizar una variedad de lenguajes y tecnologías en diferentes servicios, permitiendo a los equipos elegir las herramientas más adecuadas para sus necesidades específicas.
3. Optimización del Ciclo de Vida del Software
Equipos Autónomos: Cada equipo puede manejar todo el ciclo de vida de una característica o servicio, desde la idea hasta el mantenimiento.
Pipelines de Integración y Despliegue Rápidos: Los servicios más pequeños permiten pruebas y construcciones más rápidas, agilizando los ciclos de CI/CD.
Desventajas de los Microservicios
1. Complejidad Operativa
Dificultad de Entendimiento: La interacción entre múltiples servicios puede ser complicada y difícil de entender para nuevos desarrolladores.
Retos en la Depuración: La depuración puede ser más complicada cuando los errores atraviesan varios servicios, especialmente en comunicaciones asincrónicas como las impulsadas por eventos.
Monitoreo y Registro de Datos: Monitorear y registrar adecuadamente una aplicación distribuida requiere herramientas especializadas y puede ser intrincadamente complejo.
2. Desafíos Técnicos
Problemas de Datos Distribuidos: La consistencia de los datos puede ser un desafío, especialmente cuando los eventos se procesan en un orden no deseado, llevando a inconsistencias.
Modificaciones en Múltiples Servicios: Actualizaciones que requieren cambios en varios servicios pueden ser tediosas y propensas a errores.
Conclusión
El Spring Framework proporciona un conjunto robusto de herramientas que facilitan la implementación de microservicios, ayudando a las empresas a construir sistemas más flexibles y escalables. A pesar de los desafíos, los beneficios a largo plazo de adoptar microservicios justifican su uso en entornos que requieren alta disponibilidad y adaptabilidad.
Referencias
Newman, S. (2021). Building Microservices, 2nd Edition. O’Reilly Media, Inc.
Carnell, J. (2021). Spring Microservices in Action. Manning.
Para comprender qué son los microservicios, primero debemos discutir los sistemas monolíticos y sus desventajas.
Imagina Amazon como un proyecto de Spring Framework. Consiste en un enorme repositorio de Git, compuesto por clases y componentes como estos:
Producto
Inventario
Catálogo
Calificación
Envío
Pedido
Pago
Descuento
Recomendación
Lista de deseos
CompraConUnClic
Pregúntate: ¿qué tiene que ver la funcionalidad de compra con un clic con el envío? La compra con un clic se enfoca en números de tarjeta de crédito e información del usuario, mientras que el envío se ocupa de atributos del producto, como peso, altura y direcciones. De manera similar, la lógica de reembolso no tiene nada que ver con una lista de deseos.
A pesar de que estos componentes son muy distintos entre sí, coexisten en el mismo repositorio, se pueden acceder a través de la misma API, pueden llamarse entre sí y compartir algunas funciones o clases generales.
Están estrechamente acoplados y tienen baja cohesión. Sin embargo, en una arquitectura ideal, preferiríamos un acoplamiento débil y una alta cohesión.
Acoplamiento Débil
Si un proyecto tiene un acoplamiento débil, un cambio en una parte no debería requerir un cambio en otra. Imagina que tienes Producto, Pedido, Descuento y Pago en el mismo proyecto. Al inicio, manejas algunas clases pequeñas y elegantes. El usuario crea un pedido con algunos identificadores de productos, realizas una consulta desde Producto, calculas los precios totales y demás, todo de manera simple. Luego, el negocio desea agregar descuentos. Entonces llamas al componente Descuento desde Pedido, pasando los productos. Todo sigue simple. Un día, el negocio desea un descuento basado en el monto total de los pedidos. Sin problemas, simplemente pasas el objeto Pedido a Descuento. Después, necesitas otorgar un descuento basado en el historial de pedidos y pagos del usuario, por lo que pasas más pedidos y el objeto Usuario a Descuento. Quizás también quieras obtener algo del módulo de Pago.
Este enfoque reduce la probabilidad de efectos secundarios no deseados cuando se realizan cambios o se añaden nuevas funcionalidades, promoviendo una arquitectura más sostenible y fácil de mantener. Con un acoplamiento débil, cada módulo funciona de manera independiente, pero puede interactuar con otros cuando es necesario sin causar interrupciones o errores en otras áreas del sistema.
El Problema de los Sistemas Estrechamente Acoplados
Aunque hemos discutido las ventajas del acoplamiento débil, la realidad en muchos sistemas es que están estrechamente acoplados. Imagina ahora que en tu sistema, tienes una interacción compleja entre Pedido, Descuento, Producto, Pago, Historial de Pedidos y Usuario. En este escenario, un cambio aparentemente inocuo puede tener consecuencias inesperadas:
Modificas la función getNetPrice() en la clase Producto, y repentinamente la llamada a la API de Descuento devuelve un código de estado 500.
Cambias algo en la clase Usuario y Pedido comienza a comportarse de manera diferente.
Ajustas algo en Historial de Pedidos y obtienes los descuentos incorrectos.
Así llegamos al monolito estrechamente acoplado, donde un cambio en algún lugar a menudo rompe la aplicación en otro lugar, frecuentemente en un lugar completamente no relacionado. Este es el problema de los monolitos.
Alta Cohesión
Alta cohesión significa que el comportamiento relacionado se encuentra en un solo lugar, y el comportamiento no relacionado en otro diferente. Por ejemplo, en la clase Pedido, no querrías un método llamado getProductPrice(); eso pertenecería a la clase Producto. Mantener una alta cohesión ayuda a asegurar que, si deseas cambiar algún comportamiento, no afectes accidentalmente otras áreas del sistema. Esto facilita realizar cambios más seguros y robustos.
En el mundo de los monolitos, es muy fácil crear un alto acoplamiento y baja cohesión debido a la tendencia de agrupar muchas funciones diferentes en el mismo lugar. A medida que aumenta el número de características, clases, métodos y desarrolladores, se vuelve demasiado fácil cometer errores.
Aun cuando es posible escribir buenos monolitos de alta calidad, cada sistema, sin importar cuán bien esté hecho, tiende a enfrentar algunos problemas de acoplamiento y cohesión.
¿Qué son los microservicios?
Imagina un restaurante donde el camarero utiliza una aplicación móvil para tomar tu pedido. Él toma tu pedido, y el chef recibe la información sobre lo que debe cocinar. Cuando el chef termina, el camarero entrega tu comida, luego tú la consumes. Después, el camarero imprime tu recibo a través de la aplicación móvil. Al final del día, el gerente abre el mismo sistema en su portátil y revisa los ingresos, el ingreso neto, el costo de ingresos y finaliza el día. Cada viernes va a su supermercado favorito y compra algunos elementos del menú. Luego los registra en el sistema. Al final de cada mes, descarga un archivo de Excel sobre las finanzas y lo envía por correo electrónico al contable.
Componentes en este ejemplo de microservicios
En este dominio, tenemos componentes como:
Producto
Grupo de Productos
Elemento del Menú
Pedido
Ítem de Pedido
Recibo
Descuento
Finanzas
Almacenamiento
Compra
Primero, intentemos crear algunos grupos con estos componentes. En cada grupo, colocamos componentes similares, que están relacionados entre sí:
Grupos:
Gestión de Productos
Gestión de Pedidos
Gestión de Inventario
Gestión de Descuentos
Finanzas
Así agrupamos los componentes según su funcionalidad. En nuestra aplicación, tenemos responsabilidades como:
Gestión de Productos
Hace un seguimiento de nuestros productos y sus grupos
Almacenamiento
Gestión de Pedidos
Almacena los pedidos de los clientes
Gestión de Inventario
Gestiona elementos del menú y otros productos
Compras
Gestión de Descuentos
Maneja tarjetas de descuento y demás
Finanzas
Crea informes sobre el negocio
Ahora, coloquemos cada componente en un grupo:
Gestión de Productos
Grupo de Productos
Producto
Gestión de Pedidos
Pedido
Ítem de Pedido
Recibo
Gestión de Inventario
Elemento del Menú
Almacenamiento
Compra
Gestión de Descuentos
Tarjeta de Descuento
Finanzas
Informe de Ingresos
Informe de Costo de Ingresos
En una aplicación monolítica clásica, creas un repositorio y pones todos los componentes en él. Pero en el mundo de los microservicios, creamos un proyecto separado para cada espacio de nombres.
¿Qué es exactamente un servicio?
Un servicio es básicamente una pequeña aplicación que funciona por sí misma y tiene su propia forma de comunicarse con otras aplicaciones o servicios. Imagina que divides una gran aplicación, como un programa de gestión de un restaurante, en partes más pequeñas y manejables, donde cada parte maneja una tarea específica. Por ejemplo, una parte maneja sólo los pedidos, otra sólo las reservaciones, etc. Cada una de estas partes es un servicio.
Estos servicios no son simplemente partes de un programa más grande, son aplicaciones completas. Puedes empezar a usarlas, actualizarlas o detenerlas sin afectar a las demás. Además, cada una tiene su propia conexión al mundo exterior a través de lo que llamamos una “API”, que es como un lenguaje común que permite que diferentes programas hablen entre sí.
¿Qué hace “micro” a un microservicio?
En el desarrollo de programas, generalmente preferimos que las cosas sean lo más simples y pequeñas posible porque son más fáciles de manejar y mantener. Esto también se aplica a los servicios. Un “microservicio” es tan pequeño que un equipo pequeño puede rehacerlo completamente en unas pocas semanas si es necesario, lo cual es útil si queremos mejorar algo rápidamente sin interferir con el resto de la aplicación.
La idea es que cada microservicio sea independiente, así que si necesitas hacer cambios en uno, no tienes que preocuparte por romper otro. Esta forma de organizar las cosas viene recomendada en un libro famoso sobre el tema llamado “Building Microservices”, escrito por Sam Newman.
Ahora, supongamos que creamos cuatro microservicios diferentes para manejar diferentes partes de nuestra aplicación de restaurante. La siguiente pregunta natural es: ¿cómo interactúan entre sí? Por ejemplo, ¿cómo sabe el servicio de Finanzas cuánto dinero ha hecho el restaurante si es el servicio de Pedidos el que tiene esa información? En el siguiente posts, explicaré cómo se comunican estos servicios y cómo se aseguran de trabajar juntos de manera efectiva, pero primero, vamos a entender un poco más sobre cómo se organizan.
Encontrando el contexto delimitado
En las secciones anteriores, hablamos sobre cómo agrupar clases. Pero en el mundo real, a estos grupos los llamamos “contextos delimitados”. Esta expresión proviene del libro de Eric Evans, “Diseño Guiado por el Dominio”. Cada producto tiene su propio dominio y lenguaje, similar al que utilizamos en el ejemplo del restaurante. Cada dominio contiene varios modelos, y siempre es posible identificar un conjunto de modelos que pueden agruparse y alrededor de los cuales podemos trazar un límite. Este límite indica que estos modelos pueden funcionar juntos sin necesidad de depender de elementos externos.
En cada contexto delimitado, existen detalles de implementación internos y modelos internos que no deseamos exponer al mundo exterior. Sin embargo, también hay modelos compartidos que sí pueden ser expuestos externamente. Este es un concepto crucial. Significa que, por ejemplo, no querrías exponer todos tus componentes de Java del servicio de Productos. Prefieres mantener estos componentes dentro del servicio de productos porque contienen muchos detalles específicos de cómo funciona el servicio y deseas crear una versión más simplificada y transformada para uso externo. Profundizaremos más en este tema más adelante en este libro. Por ahora, puedes imaginar algo así:
Microservicios con Spring Boot
En la arquitectura de microservicios que estamos utilizando, el Servicio de Catálogo administra componentes internos, como ProductGroup y Product, encapsulados en un contorno rojo para indicar su uso exclusivo dentro de este servicio.
El modelo compartido, llamado Shared Product Model, es un componente externo, representado en color verde. Este modelo es utilizado para compartir información esencial de los productos con otros servicios, facilitando así la integración entre diferentes partes de la aplicación sin exponer los detalles internos del servicio de catálogo. Como puede observarse, este modelo se comparte con el Servicio de Pedidos, permitiendo una interacción eficiente entre los servicios.
El Servicio de Pedidos, también en color rojo, utiliza el Shared Product Model para acceder a los datos necesarios del producto que necesita para procesar pedidos, gestionar los ítems de los pedidos y generar los recibos. Este servicio gestiona entidades como Order, OrderItem y Receipt, que son específicas de su dominio y no están expuestas más allá de sus límites.
Identificar correctamente el contexto delimitado es esencial en el diseño de microservicios, y es una habilidad crucial en su implementación efectiva. Esta delimitación adecuada facilita la mantenibilidad, escalabilidad y seguridad de las aplicaciones, permitiendo que cada servicio se mantenga enfocado en su funcionalidad principal sin incurrir en dependencias complicadas con otros servicios.
La ventaja significativa de los microservicios es la modularidad y el aislamiento que permiten a los equipos trabajar de manera más independiente y eficiente, optimizando la gestión de cada parte de la aplicación como un módulo separado.
Conclusión
Adoptar microservicios con Spring Framework permite superar los problemas inherentes a los sistemas monolíticos, como la dificultad en mantenimiento y escalabilidad. Utilizando Spring, las aplicaciones se dividen en servicios más pequeños y manejables, cada uno operando dentro de su propio contexto delimitado. Esto promueve un desarrollo, despliegue y escalado más ágil y específico para cada servicio. Además, el uso de modelos compartidos facilita la integración entre servicios sin exponer detalles internos. En resumen, los microservicios con Spring Framework ofrecen una solución robusta y flexible para desarrollar aplicaciones modernas, superando los desafíos de los sistemas monolíticos y preparando a las empresas para un futuro escalable y eficiente.
Referencia
Newman, S. (2021). Building Microservices, 2nd Edition. O’Reilly Media, Inc.
Carnell, J. (2021). Spring Microservices in Action. Manning.
Evans, Eric (2004). Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley
La adopción de expresiones lambda en Kotlin ha revolucionado la manera en que desarrollamos software. Ofrecen un mecanismo poderoso y expresivo para manipular datos y comportamientos de manera concisa. Este post se sumerge en las profundidades de las lambdas en Kotlin, explorando su sintaxis, uso práctico y la potente combinación con referencias a miembros y funciones de la biblioteca estándar de Kotlin.
¿Qué son las Lambdas?
Las lambdas, en esencia, son funciones anónimas que pueden ser usadas como expresiones. Permiten encapsular una porción de código para ser ejecutada en el futuro, posibilitando un código más limpio y modular. Kotlin no solo soporta lambdas sino que las integra como un componente fundamental del lenguaje, abriendo un mundo de posibilidades para los desarrolladores.
Caso de Uso: Manipulación de Eventos
Consideremos el caso de asignar un manejador de eventos a un botón en una interfaz gráfica:
button.setOnClickListener(object: OnClickListener{ override fun onClick(v: View){ println("El botón fue clickeado!") } })
Aunque funcional, este enfoque es verboso. Kotlin nos permite simplificarlo considerablemente mediante el uso de lambdas:
button.setOnClickListener{println("El botón fue clickeado!") }
Esta simplificación no solo reduce la cantidad de código sino que lo hace más legible y directo.
Lambdas y Colecciones
Una de las fortalezas de Kotlin es cómo las lambdas se integran con las colecciones, facilitando operaciones que en otros lenguajes serían tediosas y verbosas.
Ejemplo: Encontrando el Mayor Elemento
Imaginemos que tenemos una lista de Producto y queremos encontrar el de mayor precio. Sin el uso de lambdas, necesitaríamos implementar un bucle para recorrer manualmente la lista:
Este enfoque, resulta verboso y alejado de lo que realmente queremos expresar: simplemente encontrar el producto más caro.
Con lambdas, Kotlin nos brinda una solución más elegante y directamente alineada con nuestra intención:
println(products.maxByOrNull{ it.price})
En esta versión, la lambda pasada a maxByOrNull especifica que queremos comparar los productos por su propiedad price. Esto no solo reduce significativamente la cantidad de código sino que lo hace más declarativo, expresando claramente nuestra intención sin distracciones.
Simplificación de Llamadas con Lambdas en Kotlin
Kotlin proporciona una serie de optimizaciones sintácticas que nos permiten escribir código más limpio y conciso, especialmente cuando trabajamos con colecciones y funciones que requieren de predicados o acciones como parámetros. Un excelente ejemplo de esto es cuando queremos determinar el producto más caro de una lista utilizando la función maxByOrNull.
El texto que que acompaña este segmento ilustra cómo Kotlin nos brinda distintas maneras de formular una lambda que elige la propiedad a comparar, en este caso, el precio de un producto.
Examinemos cómo estas simplificaciones se aplican en la práctica con una lista de productos:
products.maxByOrNull({ p: Product -> p.price }) – La lambda completa con tipo explícito.
products.maxByOrNull() { p: Product -> p.price } – Movemos la lambda fuera de los paréntesis.
products.maxByOrNull { p: Product -> p.price } – Kotlin permite omitir los paréntesis si la lambda es el único argumento.
products.maxByOrNull { p -> p.price } – Cuando el contexto es claro, podemos omitir el tipo del parámetro.
products.maxByOrNull { it.price } – Para un solo parámetro, usamos el nombre implícito ‘it’ para más concisión.
products.maxByOrNull(Product::price) – Una referencia a miembro para la máxima simplificación y claridad.
Es crucial recordar que, aunque la convención de ‘it‘ es útil para acortar el código, su uso debe ser medido. En situaciones con lambdas anidadas, puede volverse ambiguo qué valor representa it. En tales casos, es mejor declarar los parámetros de cada lambda explícitamente. Además, si el significado o tipo del parámetro no es claro por el contexto, también es preferible una declaración explícita.
Este enfoque no solo mejora la legibilidad del código sino que también evita errores comunes que pueden ocurrir al manipular las colecciones, permitiéndonos escribir expresiones que son más declarativas y cercanas a nuestra intención original.
Referencias a Miembros: Maximizando la Simplicidad
Kotlin va más allá de las expresiones lambda al introducir las referencias a miembros, una característica que permite referenciar métodos o propiedades directamente, simplificando aún más el código cuando queremos acceder a ellos.
Sintaxis y Uso
La sintaxis para utilizar una referencia a miembro es sencilla: solo necesitamos el operador ::. Por ejemplo, si queremos referenciar la propiedad price de un objeto Product, lo haríamos de la siguiente manera: Product::price.
Imaginemos que tenemos una lista de productos y queremos obtener una lista de sus precios. En lugar de definir una lambda que acceda a la propiedad price de cada producto, podemos usar una referencia a miembro para hacerlo de forma más directa y legible:
Este método no solo resulta ser más conciso, sino que también mejora la legibilidad al reducir la necesidad de declarar explícitamente una función lambda solo para acceder a una propiedad. Así, las referencias a miembros en Kotlin nos ofrecen una forma elegante y eficiente de trabajar con propiedades y métodos de objetos.
La imagen de la figura #1 que se muestra a continuación ilustra visualmente cómo se forma una referencia a miembro en Kotlin. Como puede verse, la sintaxis utiliza el operador :: para vincular directamente la clase Producto con su miembro precio, destacando la simplicidad y la potencia de este enfoque.
Figura #1
Este diagrama muestra la estructura de Producto::precio, donde Producto es la clase y precio es el atributo que queremos referenciar. La separación por dos puntos dobles es lo que define la referencia a miembro en Kotlin, proporcionándonos una manera directa y legible de acceder a las propiedades de nuestros objetos dentro de diversas operaciones funcionales.
Lambdas en Kotlin con Receptores
Kotlin también introduce el concepto de lambdas con receptores, lo que nos permite llamar métodos en un objeto específico dentro de la lambda, extendiendo aún más la flexibilidad y potencia de las lambdas.
Ejemplo Práctico: Configuración de Objetos
Un caso de uso común es la configuración de objetos al momento de su creación. Kotlin nos facilita este patrón a través de la función apply:
valmyTextView=TextView(context).apply{ text="Texto de ejemplo" textSize= 16f setPadding(10,0,10,0) }
Aquí, apply nos permite configurar el objeto TextView recién creado de una manera fluida y natural, manteniendo nuestro código limpio y conciso.
Conclusión
Las lambdas y referencias a miembros en Kotlin son herramientas poderosas que te permiten escribir código más limpio, conciso y expresivo. Al integrar estas características de manera profunda en el lenguaje, Kotlin no solo facilita patrones comunes de programación sino que abre nuevas posibilidades para la abstracción y reutilización de código. Ya sea que estés manipulando colecciones, gestionando eventos de usuario o simplemente necesitas pasar comportamiento alrededor, las lambdas en Kotlin te ofrecen una forma elegante y eficiente de hacerlo.
Dominando más funciones Lambdas en Kotlin: ‘with’, ‘apply’ y ‘also’
En esta segunda parte exporaremos las características y uso de más funcioes Lambdas en Kotlin.
Ejecutando múltiples acciones sobre un objeto: with
La función with de Kotlin es una joya para quienes buscan claridad y concisión en su código. A diferencia de muchos lenguajes que requieren estructuras verbosas, with permite actuar sobre un objeto con múltiples operaciones en un contexto temporal, sin tener que repetir el nombre del objeto. Esto es especialmente útil en situaciones donde se requiere configurar un objeto con varios pasos de inicialización o modificaciones.
Aplicación Práctica de with con Lambdas en Kotlin
Imagina que necesitas componer un informe detallado de un producto, con varias características que debes concatenar. Podrías hacerlo con una serie de operaciones sobre un StringBuilder, pero with te permite hacerlo de forma más elegante:
funproductReport(product: Product): String=with(StringBuilder()) {append("Reporte del Producto: ${product.name}\n")append("Precio: ${product.price}\n")append("Categoría: ${product.category}\n")append("Calificación: ${product.rating}\n")toString()}
En este refactorizado, StringBuilder actúa como el receptor dentro de la lambda de with, permitiéndote llamar directamente a append sin repetir result.
Refinamiento con with
La llamada a with se presenta tan natural en el código que parece ser parte del lenguaje mismo. La primera entrada para with define el contexto del receptor, mientras que la segunda entrada es la lambda que describe las operaciones a realizar.
Lambdas en Kotlin con Receptor: Extensión de Funcionalidad
El paradigma de las lambdas con receptor se extiende más allá del uso de with. Es similar a escribir una función de extensión: proporciona un receptor implícito, permitiendo un acceso directo a sus miembros y métodos.
Inicialización y Configuración con apply
apply lleva la inicialización de objetos a otro nivel en Kotlin. Es ideal cuando tienes un objeto y quieres aplicarle una serie de operaciones de configuración antes de utilizarlo.
Digamos que estás estableciendo un DataLogger para registrar información en un archivo. Con apply, puedes configurarlo de manera limpia y legible:
El resultado de apply es el propio objeto DataLogger, ya configurado y listo para usar.
Uso Práctico de apply
La capacidad de apply para devolver el objeto configurado lo hace ideal en la creación de objetos detallados. Por ejemplo, si estás configurando una instancia de un objeto User, apply te permite hacerlo todo en una declaración:
Con also, Kotlin proporciona una manera de realizar operaciones adicionales en objetos sin alterar el flujo principal del código. Es similar a apply, pero en lugar de usar el receptor implícito this, usa el parámetro it, que representa el objeto.
Refactorización con also
Supongamos que estás procesando una lista de países para un análisis geopolítico. Deseas registrar cada paso sin modificar la lista principal:
valcountries=listOf("Colombia","Nicaragua","Peru","Brasil") valprocessedCountries=countries.filter{it.length >5}.also{log("Países con nombres largos: $it") }.map{it.uppercase() }.also{log("Países en mayúsculas: $it") }.sorted()
Aquí, also se usa para efectos colaterales como el registro o la depuración, permitiendo una visión clara de cada etapa del procesamiento.
Conclusión de Lambdas en Kotlin
En la continuación de nuestra exploración de la programación funcional en Kotlin, nos sumergimos más profundamente en cómo Kotlin simplifica la escritura de código claro y eficiente. En esta segunda parte, descubrimos las potentes funciones ‘with’, ‘apply’ y ‘also’ que Kotlin ofrece para realizar operaciones en los objetos sin la redundancia del código imperativo.
Exploramos ‘with’ para ejecutar múltiples acciones en un objeto de manera compacta, ‘apply’ para la inicialización y configuración eficiente de objetos, y ‘also’ para acciones adicionales que queremos realizar en objetos mientras mantenemos el flujo de operaciones inmutable. A través de ejemplos prácticos y una refactorización cuidadosa, mostramos cómo estas funciones mejoran la legibilidad del código y la expresividad del lenguaje. Este conocimiento avanzado eleva nuestras habilidades en Kotlin y nos prepara para construir aplicaciones robustas y mantenibles.
En este artículo, vamos a crear una aplicación REST con Spring Boot conectada a una base de datos Oracle.
Herramientas
Java 17
Spring Tool Suite (STS) o Intellij IDEA
Oracle XE
Docker
Postman
Crear un proyecto Spring Boot
Vamos a ingresar a la siguiente url https://start.spring.io/, como se puede observar en la Figura #1.
Paso # 1:
Seleccionamos en el proyecto la opción Maven, el lenguaje Java, la versión de Spring Boot para este ejemplo 2.7.8
Paso # 2:
En el Project Metadata: Diligenciamos la información del proyecto, como el Group, Artifact, Nombre, si requiere de adicionar una descripción del proyecto, nombre del paquete con su respectivo empaquetamiento. Para esta demo en el Packaging seleccionamos Jar y en la versión de Java la número 17 que es la version LTS.
Figura 1
Paso # 3:
Continuando con la gestión del información del proyectó adicionamos las siguientes dependencias.
Lombok: Es una librería java que contiene anotaciones que generan automáticamente métodos de ayuda después de la compilación como por ejemplo (setters, getters, constructores).
Spring Web: Proporciona características de integración comunes específicas de la web, para construir aplicaciones web, incluyendo RESTful, utilizando Spring MVC.
Spring Data JPA: Proporciona soporte de repositorio para Java Persistence API (JPA). Facilitando el desarrollo de aplicaciones que necesitan acceder a fuentes de datos JPA.
Spring Boot DevTools: Proporciona reinicio rápido de las aplicaciones.
Oracle Driver: Driver que proporciona acceso a Oracle.
Validator: Es el estándar para implementar la lógica de validación en el ecosistema Java, por ejemplo lo utilizamos en la clase validando los campos con sus respectivas restricciones. Por otro lado está bien integrado con Spring y Spring Boot.
Spring-boot-starter-parent: Todos los proyectos de spring boot construidos en maven utilizan spring boot starter parent en su pom.xml. Estas son algunas acciones que realiza :
Configurar la versión y las propiedades de Java.
Gestionar las dependencias y sus versiones.
Paso #4:
Hacemos click para generar el proyecto.
Revisamos la estructura del proyecto como se puede visualizar en la Figura #2, debería quedar algo así.
Figura 2
En este punto vamos a revisar el archivo pom.xml como se muestra en la Figura #3.
En el siguiente paso vamos a crear los paquetes con sus respectivas clases e interfaces.
1. Entidad Estudiante
Creamos un paquete con el nombre model y dentro de el una Clase con el nombre Estudiante que contendrá toda la información básica sobre el estudiante, como la identificación, nombre, apellido, dirección, país, ciudad, edad.
Contendrá los métodos CRUD habituales y más para manejar las operaciones básicas. Por lo tanto, para crear nuestro repositorio personal, necesitamos extender el repositorio JPA de Spring Data. Permite que los servicios se comuniquen con la base de datos.
La aplicación Spring Boot del lado del Backend utiliza Clases de Servicio para representan el núcleo de su aplicación. Para este ejemplo crearemos una interfaz donde crearemos todos los métodos para realizar las operaciones de la lógica de negocio.
package com.geovanny.code.service;
import java.util.List;
import java.util.Optional;
import com.geovanny.code.model.Estudiante;
public interface EstudianteService {
public List<Estudiante> findEstudianteAll();
public Estudiante createEstudiante(Estudiante estudiante);
public Estudiante updateEstudiante(Estudiante estudiante);
public void deleteEstudiante(Long id);
public Optional<Estudiante> getEstudiante(Long id);
}
3.1 Implementación del Servicio
En esta clase se implementa toda la lógica de negocio de nuestro ejemplo de estudiante, adicionamos la anotación @Service e implementamos la clase desde la interfaz de servicio.
package com.geovanny.code.service;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.geovanny.code.model.Estudiante;
import com.geovanny.code.repository.EstudianteRepositorio;
@Service
public class EstudianteServiceImpl implements EstudianteService{
@Autowired
private EstudianteRepositorio estudianteRepositorio;
@Override
public List<Estudiante> findEstudianteAll() {
return estudianteRepositorio.findAll();
}
@Override
public Estudiante createEstudiante(Estudiante estudiante) {
return estudianteRepositorio.saveAndFlush(estudiante);
}
@Override
public Estudiante updateEstudiante(Estudiante estudiante) {
return estudianteRepositorio.save(estudiante);
}
@Override
public void deleteEstudiante(Long id) {
estudianteRepositorio.deleteById(id);
}
@Override
public Optional<Estudiante> getEstudiante(Long id) {
return estudianteRepositorio.findById(id);
}
}
4. Excepción
Para este ejemplo solamente utilizaremos la excepción NOT_FOUND, queda para el lector si desea adicionar otra tipo de excepcion por ejemplo NO_CONTENT
package com.geovanny.code.exception;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.http.HttpStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ModeloNotFoundException extends RuntimeException {
public ModeloNotFoundException(String mensaje)
{
super(mensaje);
}
}
5. Controlador
Creamos el controlador que se utilizará para interactuar con los estudiantes y realizar las acciones CRUD. Permite que los Servicios se comuniquen con el Frontend.
package com.geovanny.code.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.geovanny.code.exception.ModeloNotFoundException;
import com.geovanny.code.model.Estudiante;
import com.geovanny.code.service.EstudianteService;
import java.util.ArrayList;
import java.util.List;
import javax.validation.Valid;
@RestController
@RequestMapping("/estudiantes")
public class EstudianteController {
@Autowired
private EstudianteService service;
@GetMapping(produces = "application/json")
public ResponseEntity<List<Estudiante>> listar() {
List<Estudiante> estudiantes = new ArrayList<>();
estudiantes = service.findEstudianteAll();
return new ResponseEntity<List<Estudiante>>(estudiantes, HttpStatus.OK);
}
@PostMapping
public ResponseEntity<Estudiante> crearEstudiante(@Valid @RequestBody Estudiante estudiante){
service.createEstudiante(estudiante);
return new ResponseEntity<Estudiante>(HttpStatus.CREATED);
}
@GetMapping("/{id}")
public ResponseEntity<Estudiante> getEstudianteById(@PathVariable("id") Long id){
Estudiante estudiante= service.getEstudiante(id).orElseThrow(() -> new ModeloNotFoundException("Estudiante no encontrado $id"));
return new ResponseEntity<Estudiante>(estudiante, HttpStatus.OK);
}
@DeleteMapping("/{id}")
public ResponseEntity<Estudiante> deleteEstudiante(@PathVariable("id") Long id){
service.deleteEstudiante(id);
return ResponseEntity.ok().build();
}
@PutMapping("/{id}")
public Estudiante updateEstudiante(@PathVariable("id") Long id, @Valid @RequestBody Estudiante estudiante){
Estudiante dbestudiante = service.getEstudiante(id).orElseThrow(() -> new ModeloNotFoundException("Estudiante No enocntrado"));
dbestudiante.setNombres(estudiante.getNombres());
dbestudiante.setApellidos(estudiante.getApellidos());
dbestudiante.setDireccion(estudiante.getDireccion());
dbestudiante.setEdad(estudiante.getEdad());
dbestudiante.setCiudad(estudiante.getCiudad());
dbestudiante.setPais(estudiante.getPais());
return service.updateEstudiante(dbestudiante);
}
6. Configuración de Application.yml
En este archivo .yaml se configura el puerto y la conexión a la base de datos de oracle.
Crearemos una carpeta en dentro de resource con el nombre script y adicionamos el siguiente código en sql que nos permite crear una tabla en la base de datos.
CREATE TABLE TBL_ESTUDIANTES(
ID NUMERIC(4) NOT NULL PRIMARY KEY,
NOMBRES VARCHAR2(60) NOT NULL UNIQUE,
APELLIDOS VARCHAR2(300) NOT NULL,
DIRECCION VARCHAR2(150) NULL,
EDAD INTEGER NOT NULL,
CIUDAD VARCHAR2(50),
PAIS VARCHAR2(50),
CONSTRAINT PK_TBL_ESTUDIANTES PRIMARY KEY (ID)
);
CREATE SEQUENCE SEQ_ESTUDIANTES;
8.1 Insertar registro en la tabla de la base de datos
Esquema para insertar registros en la tabla de tbl_estudiantes.
INSERT INTO TBL_ESTUDIANTES (ID, NOMBRES, APELLIDOS, DIRECCION, EDAD, CIUDAD, PAIS) VALUES (SEQ_ESTUDIANTES.NEXTVAL, 'GEOVANNY', 'MENDOZA', 'Calle 41 San Jose',28,'Barranquilla', 'Colombia');
INSERT INTO TBL_ESTUDIANTES (ID, NOMBRES, APELLIDOS, DIRECCION, EDAD, CIUDAD, PAIS) VALUES (SEQ_ESTUDIANTES.NEXTVAL, 'OMAR', 'BERROTERAN', 'Bo grenada Casa C22',27,'Managua', 'Nicaragua');
COMMIT;
9. Gestión de Base de Datos
En esta sección exploraremos la gestión de la base de datos con el mismo intellij como se puede observar en la figura #4.
Figura #4
9.1 Conectar a la interfaz de la base de Datos
Después seleccionar la Database adicionamos la conexión con nuestra base de datos en oracle como se muestra en la figura #5.
Figura #5
Seleccionamos el signo (+) y nos ubicamos donde dice Data Source.
Seleccionamos la base de datos Oracle.
9.2 Configuración el Data Sources y Drivers
En este paso ingresamos el user y password, para este ejemplo utilizamos system como user y el password oracle como se muestra en la figura #6.
Figura #6
9.3 Ejecutar la aplicación
En este paso ejecutamos la aplicación para crear la tabla de estudiante como se muestra en la figura #7.
Nota: Tener el servicio de docker ejecutando
Figura #7
Abrimos la console para realizar una consulta sobre la tabla tbl_estudiante como se muestra en la figura #8.
Figura #8
10. Prueba con Postman
Para este ejemplo abrimos postman e importamos el archivo de postman para configurar las pruebas. Esge archivo se encuentra en el proyecto de git y se encuentra en la siguiente url Blog.postman_collection.json.
En esta sección se visualiza las pruebas exitosas de postman con los métodos GET y POST como se muestran en la figura #9 y #10.
11.1 Prueba del Método GET: Acá se puede visualizar el resultado de la prueba del método GET donde nos muestra todo los estudiantes registrados, como se muestra en la figura #9
Figura #9
11.2 Prueba del Método POST: Acá se puede visualizar la creación del estudiante con el método POST como se puede observar en la figura #10.
Conclusión
En este artículo hemos construido una aplicación REST con servicios básicos que se conectara a una base de datos Oracle, donde se crea una imagen en docker.
La programación reactiva ha estado sonando muy fuerte recientemente. Entenderla para saber cuándo usarla y cuándo no, es esencial para un desarrollador. En este artículo les explicaremos cómo crear una aplicación de Spring Boot Reactiva en poco más de 4 pasos.
Nosotros y terceros seleccionados utilizamos cookies o tecnologías similares con fines técnicos y, con su consentimiento, para otras finalidades (“interacciones y funcionalidades básicas”, “mejora de la experiencia”, “medición” y “segmentación y publicidad”) según se especifica en la política de cookies.
Usted es libre de otorgar, denegar o revocar su consentimiento en cualquier momento.
Configurar y más información