Construyendo un Sistema de Reservas de Hotel Resiliente con Spring Boot y Resilience4j
Como desarrollador Java, siempre me ha apasionado construir sistemas que no solo funcionen, sino que también sean escalables, resilientes y fáciles de mantener. He trabajado en proyectos de todo tipo, desde monolitos empresariales hasta APIs RESTful, pero ninguno ha sido tan transformador como mi incursión en microservicios con el Sistema de Reservas de Hotel. Este proyecto me permitió dominar 3 patrones de resiliencia (Circuit Breaker + Retry) para reservas hoteleras, con persistencia políglota (PostgreSQL + MongoDB). En este artículo, comparto los retos, aprendizajes y consejos prácticos para desarrolladores Java experimentados que quieran embarcarse en una transición similar hacia microservicios, con un enfoque en patrones de diseño y resiliencia.
Introducción: La Búsqueda de Resiliencia de un Desarrollador Java
Con algunos años de experiencia en Java, he construido sistemas robustos, pero el auge de las arquitecturas nativas en la nube me motivó a explorar los microservicios. Quería ir más allá de lo convencional y aprender cómo los patrones de diseño como Circuit Breaker, Retry y Specification podían elevar la calidad de mis aplicaciones. Mi objetivo fue ambicioso: desarrollar un sistema de reservas hoteleras que manejara fallos del mundo real con elegancia, utilizando herramientas modernas como Spring Boot 3.2.4, Maven, PostgreSQL, MongoDB, Resilience4j y Swagger/OpenAPI.
El Sistema de Reservas de Hotel se convirtió en mi laboratorio personal, compuesto por tres microservicios: hotel-rooms-service
(gestión de habitaciones), hotel-reservations-service
(reservas) y hotel-payments-service
(pagos). Este proyecto no solo se trató de programar, sino de adoptar una mentalidad distribuida, integrar persistencia políglota y desplegar con Docker. A lo largo del camino, enfrenté desafíos significativos, aprendí lecciones valiosas y viví momentos que me hicieron replantear mi enfoque como desarrollador.
Los Retos: Navegando por el Laberinto de los Microservicios
La transición a microservicios no fue sencilla. Aquí detallo los principales obstáculos que encontré y las lecciones que me dejaron:
Reto 1: Definir los Límites de los Microservicios
Proveniente de un entorno monolítico, me costó delimitar las responsabilidades de cada microservicio. ¿Debería el servicio de pagos validar reservas? ¿El servicio de habitaciones debería gestionar la disponibilidad? La complejidad de los sistemas distribuidos me abrumó al principio.
Aprendizaje: Adopté principios de Diseño Dirigido por el Dominio (DDD) para establecer contextos acotados. Por ejemplo, hotel-rooms-service
se centró únicamente en el inventario de habitaciones, utilizando el Patrón Specification para filtrar disponibilidad de forma dinámica:
public List<Room> findAvailableRooms(Integer guestCount, Double maxPrice) {
Specification<Room> spec = Specification.where(RoomSpecifications.isAvailable())
.and(RoomSpecifications.hasGuestCapacity(guestCount))
.and(RoomSpecifications.hasMaxPrice(maxPrice));
return roomRepository.findAll(spec);
}
Este patrón encapsuló la lógica de filtrado, haciéndola reutilizable y fácil de probar, un pilar del código limpio en microservicios.
Reto 2: Resiliencia en un Mundo Distribuido
Los fallos en microservicios son inevitables: problemas de red, tiempos de espera en bases de datos o caídas de servicios externos. Mis primeros intentos de procesar pagos eran frágiles, colapsando ante errores transitorios.
Aprendizaje: Implementé patrones de resiliencia (Circuit Breaker + Retry) para reservas hoteleras con Resilience4j. El servicio de pagos fue mi campo de pruebas:
@CircuitBreaker(name = "paymentService", fallbackMethod = "processPaymentFallback")
@Retry(name = "paymentService", fallbackMethod = "processPaymentFallback")
public Payment processPayment(String reservationId, double amount) {
logger.info("Procesando pago para reservationId: {}", reservationId);
if (Math.random() > 0.7) {
throw new RuntimeException("Fallo en el servicio de pagos");
}
Payment payment = new Payment(UUID.randomUUID().toString(), reservationId, amount, "COMPLETED");
return paymentRepository.save(payment);
}
public Payment processPaymentFallback(String reservationId, double amount, Throwable t) {
logger.warn("Fallback activado para reservationId: {} debido a: {}", reservationId, t.getMessage());
return new Payment(UUID.randomUUID().toString(), reservationId, amount, "PENDING");
}
- CircuitBreaker: Evita fallos en cascada al abrir el circuito tras 5 errores (50% de 10 llamadas), redirigiendo al fallback.
- Retry: Reintenta hasta 3 veces con backoff exponencial (500ms, 1000ms, 2000ms) antes de rendirse.
-
Configuración (en
application.properties
):
resilience4j.circuitbreaker.instances.paymentService.slidingWindowSize=10
resilience4j.circuitbreaker.instances.paymentService.failureRateThreshold=50
resilience4j.retry.instances.paymentService.maxAttempts=3
resilience4j.retry.instances.paymentService.waitDuration=500
resilience4j.retry.instances.paymentService.enableExponentialBackoff=true
Esta combinación hizo que el servicio de pagos fuera robusto, manejando fallos transitorios sin comprometer la experiencia del usuario.
Reto 3: Persistencia Políglota
Usar PostgreSQL para habitaciones y reservas y MongoDB para pagos añadió complejidad. Subestimé las diferencias en modelado de datos y gestión de conexiones.
Aprendizaje: La persistencia políglota requiere planificación. Por ejemplo, hotel-rooms-service
usó JPA para datos estructurados:
@Entity
public class Room {
@Id
private String id;
private String type;
private int guestCapacity;
private double price;
private boolean available;
// Getters y setters
}
En cambio, hotel-payments-service
aprovechó la flexibilidad de MongoDB:
@Document(collection = "payments")
public class Payment {
@Id
private String id;
private String reservationId;
private double amount;
private String status;
// Getters y setters
}
Aprendí a alinear la elección de bases de datos con las necesidades del dominio: PostgreSQL para integridad relacional, MongoDB para escenarios de alta escritura como pagos.
Reto 4: Dockerizando un Proyecto Multi-Módulo
Desplegar con Docker expuso mi falta de experiencia con proyectos Maven multi-módulo. Mis primeros intentos fallaron estrepitosamente, con errores como:
ERROR: failed to solve: maven:3.9.6-openjdk-17: not found
y más tarde:
[FATAL] Non-resolvable parent POM for org.xsoto.springcloud.msvc:hotel-payments-service:0.0.1-SNAPSHOT
Aprendizaje: Los builds multi-stage y la gestión del contexto son cruciales. Este es el Dockerfile
final para hotel-payments-service
:
# Stage 1: Build the application
FROM maven:3.9.6-eclipse-temurin-17 AS builder
WORKDIR /app
COPY ../pom.xml ./pom.xml
COPY ./pom.xml ./hotel-payments-service/pom.xml
RUN mvn dependency:go-offline -B
COPY src ./hotel-payments-service/src
RUN mvn clean package -pl hotel-payments-service -am -DskipTests
# Stage 2: Create the runtime image
FROM eclipse-temurin:17.0.10_7-jre-alpine
WORKDIR /app
COPY --from=builder /app/hotel-payments-service/target/hotel-payments-service-1.0-SNAPSHOT.jar app.jar
EXPOSE 8083
ENV SPRING_DATA_MONGODB_URI=mongodb://mongo:27017/payments_db
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
La clave fue incluir el pom.xml
padre y construir desde el directorio raíz para resolver dependencias.
Una Anécdota Personal: El Desastre con Docker
Permíteme contarte un momento que me hizo más humilde. Al principio del proceso de dockerización, estaba convencido de que crear un Dockerfile
sería pan comido. Empecé con maven:3.8.6-openjdk-17-slim
, asumiendo que existía. No fue así—el build falló con image not found
. Cambié a maven:3.9.6-openjdk-17
, pero el error persistió. Luego, el golpe final: el temido Non-resolvable parent POM
. Había ignorado la estructura multi-módulo, copiando solo el pom.xml
del módulo.
Tras horas de depuración, descubrí mi error: el contexto de Docker necesitaba el pom.xml
padre, y debía verificar los tags en Docker Hub. Fue un recordatorio doloroso de no dar nada por sentado. Pero esa frustración me llevó a dominar builds multi-stage, .dockerignore
y docker-compose
. Ese traspié me enseñó a valorar los detalles de la contenerización.
Experiencia Práctica: Uniendo las Piezas
Construir el Sistema de Reservas de Hotel requirió integrar Spring Boot 3.2.4, Maven, PostgreSQL, MongoDB, Resilience4j y Swagger/OpenAPI. Así lo logré:
-
Configuración de Microservicios:
-
hotel-rooms-service
(puerto 8081, PostgreSQL): Gestiona inventario con Patrón Specification. -
hotel-reservations-service
(puerto 8082, PostgreSQL): Maneja reservas con Patrón Factory. -
hotel-payments-service
(puerto 8083, MongoDB): Procesa pagos con CircuitBreaker + Retry.
-
Docker Compose:
Un únicodocker-compose.yml
orquestó todo:
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
mongo:
image: mongo:6
ports:
- "27017:27017"
hotel-rooms-service:
build:
context: .
dockerfile: hotel-rooms-service/Dockerfile
ports:
- "8081:8081"
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/rooms_db
hotel-reservations-service:
build:
context: .
dockerfile: hotel-reservations-service/Dockerfile
ports:
- "8082:8082"
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/reservations_db
hotel-payments-service:
build:
context: .
dockerfile: hotel-payments-service/Dockerfile
ports:
- "8083:8083"
environment:
SPRING_DATA_MONGODB_URI: mongodb://mongo:27017/payments_db
Swagger/OpenAPI:
Cada servicio expuso una interfaz Swagger (por ejemplo,http://localhost:8083/swagger-ui.html
), facilitando pruebas de APIs.Pruebas:
Tests exhaustivos validaron los patrones de resiliencia:
@Test
void testProcessPayment_FallbackTriggered() {
String reservationId = "res1";
double amount = 150.0;
circuitBreaker.transitionToOpenState();
Payment result = paymentService.processPayment(reservationId, amount);
assertEquals("PENDING", result.getStatus());
}
Consejos para Desarrolladores Java en Transición
Para los desarrolladores Java que quieran dar el salto a microservicios, aquí van algunos consejos prácticos:
-
Domina los Patrones de Diseño:
- Aprende Circuit Breaker, Retry y Specification. No son solo teoría—resuelven problemas reales. Explora la documentación de Resilience4j para ejemplos prácticos.
- Consejo: Ajusta los parámetros de Retry con cuidado para no saturar servicios externos.
-
Adopta la Persistencia Políglota:
- Elige bases de datos según el dominio. En nuestro sistema, PostgreSQL aseguró integridad para habitaciones, mientras MongoDB manejó datos de pagos de alta escritura.
- Usa abstracciones de Spring Data para simplificar el código de repositorios.
-
Familiarízate con Docker:
- Usa builds multi-stage para imágenes ligeras. Las imágenes Alpine como
eclipse-temurin:17.0.10_7-jre-alpine
me ahorraron ~200 MB por servicio. - Verifica siempre los tags en Docker Hub antes de construir.
- Ejemplo: Un
.dockerignore
bien configurado:
target/ *.jar *.log .idea/
- Usa builds multi-stage para imágenes ligeras. Las imágenes Alpine como
-
Prueba sin Descanso:
- Escribe tests para patrones de resiliencia. Simula fallos para validar fallbacks.
- Usa herramientas como JaCoCo para medir cobertura y destacar tu trabajo.
-
Automatiza desde el Inicio:
- Configura un pipeline CI/CD (por ejemplo, GitHub Actions) para construir y probar imágenes Docker automáticamente.
- Ejemplo: Un flujo básico para construir imágenes:
name: Build Docker Images on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: docker-compose build
-
Aprende de los Errores:
- No temas a errores como
image not found
onon-resolvable POM
. Son oportunidades para crecer. - Lleva un registro de depuración para documentar qué funcionó y qué no.
- No temas a errores como
Conclusión: Un Viaje que Vale la Pena
La transición a microservicios con el Sistema de Reservas de Hotel cambió mi perspectiva como desarrollador. Me enseñó a construir sistemas resilientes y escalables usando 3 patrones de resiliencia (Circuit Breaker + Retry) para reservas hoteleras, con persistencia políglota (PostgreSQL + MongoDB). Los retos—definir límites, dominar resiliencia y superar Docker—fueron duros pero gratificantes. Cada error, desde tags de imágenes inexistentes hasta problemas con el POM, afinó mis habilidades y mentalidad.
A mis colegas desarrolladoras y desarrolladores Java: sumérjanse en los microservicios y los patrones de diseño. Experimenten con Spring Boot, Resilience4j y Docker. El camino puede ser accidentado, pero la capacidad de crear sistemas que prosperen bajo presión vale cada esfuerzo. Empiecen pequeño, prueben rigurosamente y automaticen sin miedo. Su próximo proyecto podría definir su carrera.
🔍 ¿Te gustaría ver el código en acción?
Explora SpringResilientHotel en GitHub:
👉 github.com/xsoto-developer/SpringResilientHotel
¿Cuál será tu próxima aventura en microservicios? ¡Comparte tus ideas en los comentarios o contáctame en GitHub!
✨ Bonus: Siéntete libre de dejar una ⭐️ o abrir un issue con sugerencias. ¡Aprecio el feedback!
Este artículo también está disponible en: Inglés