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:

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:

Principios de CDCT

  1. Contratos impulsados por el consumidor:
  1. Versionado y publicación de contratos:
  1. Integración continua:
  1. Colaboración entre equipos:

Herramientas y enfoques

Pact

Spring Cloud Contract

Dredd

Microcks, Hoverfly, Mountebank

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:
  1. payment-service build:

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?

Buenas prácticas recomendadas

Escenarios avanzados

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.

Suscríbete