Arrancamos la nueva entrega de nuestra serie sobre patrones de arquitectura de microservicios. Como en artículos anteriores, te dejo a mano los posts que hemos publicado anteriormente, por si te has perdido alguno y quieres profundizar en los patrones de arquitectura de microservicios que ya hemos analizado:

  1. Patrones de arquitectura de microservicios, ¿qué son y qué ventajas nos ofrecen?
  2. Patrones de arquitectura: organización y estructura de microservicios.
  3. Patrones de arquitectura: comunicación y coordinación de microservicios.
  4. Patrones de arquitectura de microservicios: SAGA, API Gateway y Service Discovery.
  5. Patrones de arquitectura de microservicios: Event Sourcing y arquitectura orientada a eventos (EDA).
  6. Patrones de arquitectura de microservicios: comunicación y coordinación con CQRS, BFF y Outbox.
  7. Patrones de microservicios: escalabilidad y gestión de recursos con escalado automático.
  8. Patrones de arquitectura: de monolitos a microservicios.
  9. Configuración externalizada.

En las arquitecturas actuales, la creciente demanda de despliegues frecuentes ha empujado a equipos y organizaciones a adoptar modelos distribuidos, donde cada servicio cumple un rol específico dentro de la plataforma. Sin embargo, esta libertad también trae consigo nuevos desafíos a la hora de evolucionar las APIs sin romper a otros servicios consumidores.

Para esta problemática tenemos la ayuda del patrón de arquitectura de microservicios Consumer-Driven Contract Testing, del cual veremos:

  • Cómo garantizar la compatibilidad entre servicios pese a la evolución constante de las APIs.
  • El rol del consumidor en la definición de contratos y la verificación automatizada en el pipeline del proveedor.
  • Uso de herramientas como Pact o Spring Cloud Contract para orquestar pruebas de contrato en entornos CI/CD.
diagrama visual del proceso de Consumer-Driven Contract Testing (CDCT). Representa cómo un consumidor (cliente) define expectativas en un contrato (Pact), que luego se usa para generar pruebas que serán ejecutadas contra el proveedor (API/Servicio).

Aquí vemos un diagrama visual del proceso de Consumer-Driven Contract Testing (CDCT). Representa cómo un consumidor (cliente) define expectativas en un contrato (Pact), que luego se usa para generar pruebas que serán ejecutadas contra el proveedor (API/Servicio). El resultado de estas pruebas se valida para asegurar que cumplen lo esperado, lo que significa que el contrato sigue siendo válido.

Nota: los ejemplos de código son exclusivamente para entender los conceptos.

Retos de un entorno de microservicios donde hay evolución de APIs

Un servicio puede cambiar el formato de su respuesta o añadir nuevos campos para soportar funcionalidades de negocio.

Si otro microservicio consume esa API y no se entera de la modificación, se producen errores en producción.

Mantener tests de integración que abarquen todo el ecosistema suele ser costoso y, en ocasiones, poco ágil.

Consumer-Driven Contract Testing (CDCT)

Contexto y motivación

En una arquitectura de microservicios, es frecuente que un servicio A (consumidor) requiera datos o acciones de un servicio B (proveedor).

Por ejemplo, orders-service llama a payment-service para procesar un pago, y este retorna un JSON con el estado de la transacción. Si payment-service decide cambiar la forma de ese JSON (por ejemplo: sustituir confirmationUrl por confirmLink), orders-service podría fallar al no encontrar el campo esperado.

Los tests de integración tradicionales (end-to-end) abordan este problema levantando simultáneamente todos los servicios y verificando que la interacción funcione. Pero resultan costosos en tiempo, infraestructura y coordinación, especialmente cuando hay decenas o cientos de microservicios.

El Consumer-Driven Contract Testing aporta un enfoque más ligero y escalable:

  • El consumidor define un contrato que describe las peticiones y respuestas que espera de cada endpoint del proveedor.
  • El proveedor verifica automáticamente ese contrato en su pipeline de CI/CD para asegurarse de no introducir cambios incompatibles.

Principios de CDCT

  1. Contratos impulsados por el consumidor:
  • El consumidor sabe exactamente qué parte de la respuesta usa y qué formato necesita.
  • Esto evita la sobrecarga de testear aspectos irrelevantes para el consumidor.
  1. Versionado y publicación de contratos:
  • Los contratos se almacenan en un broker (Por ejemplo: Pact Broker) o en repositorios Git.
  • Se versionan, permitiendo a cada consumidor y proveedor avanzar de forma coordinada. Un consumidor puede especificar: “necesito la versión 2 del contrato que define confirmLink en lugar de confirmationUrl”.
  1. Integración continua:
  • Cada cambio en el proveedor (nueva versión del microservicio) verifica contra los contratos existentes publicados por consumidores.
  • Si algo no encaja, el pipeline falla inmediatamente antes de que se despliegue en producción.
  1. Colaboración entre equipos:
  • Los equipos definen sus necesidades (contratos) y acuerdan la evolución de la API.
  • No se generan fricciones en producción, sino que se resuelven de antemano en fase de desarrollo.

Herramientas y enfoques

Pact

  • Librería multiplataforma (soporta Java, JavaScript, .NET, Python, etc.).
  • Genera archivos _pact _que describen las interacciones esperadas por el consumidor.
  • Incluye un “Pact Broker” para subir y versionar estos contratos y plugins para integrar en pipelines.

Spring Cloud Contract

  • Orientado a proyectos Java/Spring Boot.
  • Permite escribir contratos en Groovy o YAML. Genera stubs de servidor y verifica automáticamente las interacciones.
  • Facilita la adopción en entornos donde el stack es mayoritariamente Spring.

Dredd

  • Basado en OpenAPI/Swagger.
  • Compara las peticiones/respuestas reales de la API con la definición en OpenAPI, validando la conformidad.
  • Útil si ya se describe la API en Swagger y se quiere testear que la implementación coincide con la especificación.

Microcks, Hoverfly, Mountebank

  • Ofrecen funciones de service virtualization y validación de contratos, aunque se suelen usar más para mocking de servicios en integración que para CDCT puro.

Ejemplo práctico

Veamos un caso concreto en el que orders-service consume el endpoint /pay de payment-service.

El escenario de negocio es: el usuario confirma el pedido, orders-service llama a payment-service con un cuerpo JSON que incluye el importe total, el ID del pedido, la forma de pago, etc. payment-service responde con un ID de pago (paymentId), un estado (APPROVED, DECLINED) y una URL de confirmación.

Definición del contrato en el consumidor

En orders-service, escribimos un test de contrato con Pact. Supongamos que usamos JUnit + Pact-JVM. Podemos hacerlo con Java o Groovy.
Un ejemplo simplificado en Java:

@ExtendWith(SpringExtension.class)
@PactTestFor(providerName = "payment-service", port = "8080")
class PaymentContractTest {

    @Pact(consumer = "orders-service", provider = "payment-service")
    public RequestResponsePact createPaymentPact(PactDslWithProvider builder) {
        return builder
            .uponReceiving("A request to create a payment")
                .path("/pay")
                .method("POST")
                .body("{\"orderId\":1234,\"amount\":99.90,\"method\":\"CREDIT_CARD\"}")
            .willRespondWith()
                .status(200)
                .body("{\"paymentId\":\"abc-123\",\"status\":\"APPROVED\",\"confirmationUrl\":\"https://payments.example.com/confirm/abc-123\"}")
            .toPact();
    }

    @Test
    @PactTestFor(pactMethod = "createPaymentPact")
    void verifyCreatePaymentPact(MockServer mockServer) {
        RestTemplate restTemplate = new RestTemplate();
        Map<String, Object> request = new HashMap<>();
        request.put("orderId", 1234);
        request.put("amount", 99.90);
        request.put("method", "CREDIT_CARD");

        ResponseEntity<Map> response = restTemplate.postForEntity(
            "http://localhost:" + mockServer.getPort() + "/pay",
            request,
            Map.class
        );

        assertEquals(200, response.getStatusCodeValue());
        assertEquals("APPROVED", response.getBody().get("status"));
        assertTrue(response.getBody().containsKey("confirmationUrl"));
    }
}

Al ejecutar este test en orders-service, Pact genera un archivo .json (el pact file) que describe la interacción:

{
  "provider": {
    "name": "payment-service"
  },
  "consumer": {
    "name": "orders-service"
  },
  "interactions": [
    {
      "description": "A request to create a payment",
      "request": {
        "method": "POST",
        "path": "/pay",
        "body": {
          "orderId": 1234,
          "amount": 99.90,
          "method": "CREDIT_CARD"
        }
      },
      "response": {
        "status": 200,
        "body": {
          "paymentId": "abc-123",
          "status": "APPROVED",
          "confirmationUrl": "https://payments.example.com/confirm/abc-123"
        }
      }
    }
  ]
}

Este archivo se puede publicar en un Pact Broker con un script Gradle/Maven que lo suba en la fase de post-test. Podemos asociarlo a la versión concreta del consumidor (por ejemplo: orders-service v1.3.0).

Verificación del contrato en el proveedor

Por su parte, payment-service (el proveedor) integra en su pipeline un paso para bajar el pact file más reciente del broker y ejecutarlo contra su implementación real o simulada. Supongamos que tenemos un test PaymentProviderTest que arranca la aplicación real en un puerto random y hace que Pact replique la interacción.
En Groovy:

class PaymentProviderTest extends Specification {

    @Shared
    @AutoStart // Supongamos una extensión que inicia el servicio de pago
    ApplicationUnderTest paymentApp = new PaymentAppUnderTest()

    @PactVerification(provider = "payment-service", consumer = "orders-service")
    def "debería validar el pacto con el servicio de pedidos"() {
        // No es necesario escribir lógica, Pact se encarga de desencadenar la interacción

        expect:
        // Pact validará la solicitud y la respuesta real
        true
    }
}

La verificación comprobará que, al llamar a POST /pay con ese body, payment-service devuelva paymentId, status y confirmationUrl tal como se definió.

Si algún atributo difiere (por ejemplo, el servicio cambió confirmationUrl → link), la verificación fallará y la build no seguirá adelante.

De este modo, el equipo de payment-service se entera inmediatamente de que el cambio planeado rompería la expectativa de orders-service, permitiendo negociar o versionar la API antes de causar una falla en producción.

Integración en pipelines de CI/CD

  1. orders-service build:
  • Corre los tests unitarios y de contrato.
  • Si pasan, genera el archivo pact y lo publica en el broker con la etiqueta master o con la versión del consumidor.
  1. payment-service build:
  • Descarga los pact files de sus consumidores (orders-service, quizá otros) desde el broker.
  • Corre la verificación contra su implementación local.
  • Si todo es correcto, se “etiqueta” la versión del proveedor como compatible con la versión X de orders-service.
  • Se procede al despliegue en entornos mayores. Si falla, se detiene el pipeline.

Esta metodología se extiende a todos los servicios que dependan de payment-service, asegurando que ninguno se rompa por un cambio inesperado.

¿Cuáles son las ventajas?

  • Detección temprana de rupturas: los proveedores saben al instante si un cambio rompe a un consumidor.
  • Menos dependencias en pruebas: no hace falta levantar todo el entorno de microservicios para comprobar la compatibilidad.
  • Documentación viva: los contratos actúan como la especificación real de qué espera cada consumidor, facilitando la comunicación entre equipos.
  • Alineamiento con despliegues independientes: cada microservicio puede evolucionar sin miedo a introducir breaking changes inadvertidos.

Buenas prácticas recomendadas

  • Adoptar un enfoque “Secure by Design”: para la configuración externalizada, protege especialmente los datos sensibles con Vault, Secrets de Kubernetes o cifrado robusto.
  • Versionar las APIs: CDCT no elimina la necesidad de versionar endpoints si se introducen cambios radicales. Puedes, por ejemplo, mantener /v1/pay y /v2/pay en payment-service, permitiendo que los consumidores migren progresivamente.
  • Mantener la comunicación entre equipos: el éxito de CDCT depende de la colaboración. No es un sustituto de la comunicación, sino un catalizador para discutir contratos de manera objetiva.
  • Automatizar el pipeline: tanto la recopilación de configuraciones como la verificación de contratos deben ejecutarse en cada build, no solo antes de una release.

Escenarios avanzados

  • Multi-Broker: en organizaciones grandes, puede haber múltiples Pact Brokers o repositorios de contratos. Asegúrate de unificar la estrategia de publicación y versionado para no generar caos.
  • Microservicios políglota: si la plataforma tiene servicios en Java, Node.js, Python, etc., asegúrate de elegir herramientas de CDCT que soporten varios lenguajes (Pact es bastante flexible en ese sentido).
  • Test contract-first: algunos equipos definen primero el contrato y generan stubs de forma automática para que los consumidores desarrollen. Otros lo hacen consumer-first. Ambas estrategias pueden convivir y complementarse.

Conclusiones y siguientes pasos

Con el Consumer-Driven Contract Testing, nos anticipamos a las roturas de API, fomentando la colaboración entre los equipos de desarrollo y evitando costosos fallos en producción.

Empodera a los consumidores para que definan su contrato, que luego se valida continuamente en el pipeline de los proveedores, evita sorpresas de última hora en producción y fomenta la colaboración inter-equipos. Además, herramientas como Pact, Spring Cloud Contract o Dredd facilitan la adopción técnica.

Si temes rupturas de API y ya has sufrido incidentes en producción, puedes empezar a documentar las interacciones principales entre tus servicios. Define uno o dos contratos en la forma de CDCT y configura el pipeline del proveedor. Verás cómo se detectan problemas antes de que impacten a los usuarios finales.

Un microservicio robusto también requiere un buen diseño de logs, trazas distribuidas (observabilidad) y mecanismos de resiliencia (circuit breakers, timeouts). CDCT no opera en aislamiento, sino que se suma a la estrategia global de DevOps/Cloud Native.

En entregas futuras, profundizaremos en temas como patrones de seguridad, tolerancia a fallos, monitoreo y observabilidad, imprescindibles para una plataforma de microservicios que opere a gran escala y cumpla exigencias de alto rendimiento y cumplimiento normativo.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.