14. Ejemplo práctico: sistema basado en microservicios

Para consolidar los conceptos del tutorial diseñaremos una plataforma de comercio electrónico llamada TiendaNova. El objetivo es mostrar cómo dividir el dominio en servicios, definir contratos claros, preparar el despliegue y habilitar la observabilidad básica. A lo largo del desarrollo incluiremos fragmentos de código en Java, descripciones de APIs y configuraciones de infraestructura que sirven como punto de partida para proyectos reales.

14.1 Diseño general del ejemplo

El ecosistema de TiendaNova se organiza en tres dominios principales: catálogo, pedidos y pagos. Cada microservicio es responsable de una parte del viaje del cliente, cuenta con su propia base de datos y expone APIs independientes. La comunicación directa se realiza mediante REST y los eventos se emplean para sincronizar datos no críticos en tiempo real, como la generación de recomendaciones.

La plataforma se complementa con un gateway que centraliza la seguridad, un sistema de identidad externo y un conjunto de herramientas de monitoreo y logging. El siguiente diagrama conceptual resume la interacción entre servicios:

[Cliente] --> [API Gateway] --> [Catalog Service]
                                   |--> [Order Service] --> [Payment Service]
                                   '--> [Notification Service]

[Order Service] -- evento --> [Recommendation Service]
[Payment Service] -- logs --> [Central Log]
[Todos] -- métricas --> [Prometheus]

Esta arquitectura enfatiza la independencia de despliegue y la extensibilidad. Si se necesita agregar un Fulfillment Service para coordinar envíos, basta con publicar nuevos eventos desde el servicio de pedidos y suscribirse a ellos.

14.2 Definición de servicios, APIs y bases de datos

Cada microservicio se implementa con un conjunto de controladores, lógica de dominio y repositorios. Los modelos de datos se diseñan según la necesidad del dominio, evitando compartir esquemas. El catálogo utiliza una base documental para almacenar productos de forma flexible, mientras que pedidos y pagos se apalancan en bases relacionales para garantizar integridad transaccional.

14.2.1 API del servicio de catálogo

El servicio de catálogo expone endpoints para listar productos, obtener detalles y administrar el inventario. La siguiente interfaz resume la definición de la API.

@RestController
@RequestMapping("/api/catalog")
class CatalogController {

    private final CatalogService service;

    CatalogController(CatalogService service) {
        this.service = service;
    }

    @GetMapping
    public Page<ProductDto> list(@RequestParam(defaultValue = "0") int page,
                                 @RequestParam(defaultValue = "12") int size) {
        return service.findAll(page, size);
    }

    @GetMapping("/{sku}")
    public ProductDto detail(@PathVariable String sku) {
        return service.findBySku(sku);
    }

    @PostMapping
    public ResponseEntity<ProductDto> create(@RequestBody CreateProductCommand command) {
        ProductDto created = service.create(command);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }
}

El contrato REST se documenta con un esquema OpenAPI para facilitar la generación de clientes y pruebas automáticas. El servicio persiste productos en una colección products dentro de una base NoSQL, incluyendo precios, categorías y atributos dinámicos.

14.2.2 Modelo de datos para pedidos y pagos

Los pedidos se registran en una base relacional con tablas orders y order_items. El servicio de pagos mantiene su propio esquema con la tabla payments y estados de autorización. Una vez confirmado un pago, el servicio de pedidos actualiza su estado y publica un evento que habilita la preparación del pedido.

@Entity
@Table(name = "orders")
public class OrderEntity {

    @Id
    private String id;
    private String customerId;
    private BigDecimal total;
    private Instant createdAt;
    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    @OneToMany(mappedBy = "orderId", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItemEntity> items = new ArrayList<>();
}

Al mantener bases independientes se reduce la necesidad de transacciones distribuidas. Las integraciones se realizan a través de eventos y APIs idempotentes.

14.3 Despliegue con Docker Compose o Kubernetes

Para acelerar el desarrollo local se utiliza Docker Compose, que levanta los servicios y sus dependencias con un comando. En entornos de mayor escala se adopta Kubernetes para gestionar la disponibilidad, escalado y actualizaciones.

14.3.1 Stack local con Docker Compose

El siguiente archivo define un entorno que incluye los microservicios, una base PostgreSQL y un broker de eventos.

version: "3.9"
services:
  catalog-service:
    build: ./catalog-service
    ports:
      - "8081:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=docker
  order-service:
    build: ./order-service
    ports:
      - "8082:8080"
    depends_on:
      - postgres
      - kafka
  payment-service:
    build: ./payment-service
    ports:
      - "8083:8080"
    depends_on:
      - postgres
      - kafka
  postgres:
    image: postgres:15
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    ports:
      - "5432:5432"
  kafka:
    image: bitnami/kafka:3
    environment:
      - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
    ports:
      - "9092:9092"
  zookeeper:
    image: bitnami/zookeeper:3.8
    ports:
      - "2181:2181"

Con este archivo se lanza el ecosistema ejecutando docker compose up. Las variables de entorno se adaptan para conectar cada servicio a la infraestructura compartida.

14.3.2 Deployment en Kubernetes

Para el entorno de producción se crean manifiestos que definen Deployments, Services y ConfigMaps. A continuación se muestra un extracto del despliegue del servicio de pedidos con Horizontal Pod Autoscaler.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: registry/tiendanova/order-service:1.2.0
          ports:
            - containerPort: 8080
          envFrom:
            - configMapRef:
                name: order-service-config
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60

El despliegue se integra con pipelines de CI/CD que actualizan las imágenes y supervisan el estado del rollout, permitiendo rollback automático si la nueva versión se degrada.

14.4 Monitoreo e integración de logs

La operación confiable del sistema requiere recopilar métricas, trazas y logs centralizados. Los servicios exponen métricas para Prometheus, envían registros estructurados a Loki y publican trazas mediante OpenTelemetry hacia Jaeger. Esta combinación permite diagnosticar problemas en tiempo real.

14.4.1 Configuración de promotores de métricas

En cada servicio se habilita el endpoint de métricas y se configura un scrape job en Prometheus.

scrape_configs:
  - job_name: "tiendanova"
    metrics_path: "/actuator/prometheus"
    static_configs:
      - targets:
          - "catalog-service:8080"
          - "order-service:8080"
          - "payment-service:8080"

Las métricas clave (latencia, errores y tráfico) alimentan tableros de Grafana y alertas automáticas. Para los logs se utiliza un agente que envía los registros JSON a una instancia centralizada, lo que facilita correlacionar eventos entre servicios.

14.4.2 Trazas con OpenTelemetry

Los servicios se instrumentan con el SDK de OpenTelemetry, configurando un exportador OTLP y propagación de identificadores de trazas a través del gateway. De esta manera se pueden seguir las transacciones de compra de extremo a extremo y localizar cuellos de botella.