Saltar a contenido

Sesión 4 — Cambiar de proveedor sin tocar el negocio

Rama: sesion-4 Lo que vas a lograr: comprobar con tus manos la promesa de la charla. Vamos a cambiar la pasarela de pago por otra, y el dominio, el servicio y los tests no se tocan. Ni una línea.

Acordate del primer dolor de la historia: cambiar un proveedor rompía el negocio. Esta sesión es la respuesta a ese dolor.


Parte 1 — Aparece un segundo proveedor (10 min)

Imaginá que el negocio decide cambiar de pasarela: el Proveedor B es más barato. En la arquitectura vieja, esto significaba abrir el servicio y reescribirlo. Acá, significa escribir un adaptador nuevo. Nada más.

En infrastructure/adapter/out/gateway, agregá:

ProviderBClient.kt:

package com.baqjug.payments.infrastructure.adapter.out.gateway

import org.springframework.cloud.openfeign.FeignClient
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody

@FeignClient(name = "provider-b", url = "\${providers.b.url}")
interface ProviderBClient {
    @PostMapping("/v2/charges")
    fun charge(@RequestBody request: ChargeApiRequest): ChargeApiResponse
}

ProviderBAdapter.kt:

package com.baqjug.payments.infrastructure.adapter.out.gateway

import com.baqjug.payments.application.port.out.ChargeResult
import com.baqjug.payments.application.port.out.PaymentGateway
import com.baqjug.payments.domain.model.Payment
import org.springframework.context.annotation.Primary
import org.springframework.stereotype.Component

@Component
@Primary
class ProviderBAdapter(
    private val client: ProviderBClient
) : PaymentGateway {
    override fun charge(payment: Payment): ChargeResult {
        val response = client.charge(ChargeApiRequest(payment.amount.value))
        return ChargeResult(response.approved, response.reference)
    }
}

Kotlin al paso (y Spring): @Primary

Ahora hay dos clases que implementan PaymentGateway. Con @Primary le decimos a Spring cuál inyectar por defecto. El día de mañana esto puede ser una propiedad de configuración o un perfil, pero la idea es la misma: elegir el adaptador es una decisión de infraestructura, no de negocio.

Agregá la URL del nuevo proveedor en application.yml. El Proveedor B vivirá en el puerto 8082 con la ruta /v2/charges:

providers:
  a:
    url: http://localhost:8081
  b:
    url: http://localhost:8082

Parte 2 — Preparar el provider-sim para el Proveedor B (10 min)

El ProviderBAdapter llama por Feign a http://localhost:8082/v2/charges. Si lo dejás así y corrés un pago, vas a ver este error:

feign.RetryableException: Connection refused executing POST http://localhost:8082/v2/charges

Es esperable: no hay nada escuchando en el 8082 todavía. Vamos a hacer que el provider-sim simule también al Proveedor B. En la vida real serían dos servicios externos distintos. Para el taller, un solo simulador con las dos rutas alcanza.

1. Agregá el endpoint /v2/charges en el ChargeController.kt del provider-sim:

@PostMapping("/v2/charges")
fun chargeV2(@RequestBody req: ChargeRequest): ChargeResponse =
    ChargeResponse(approved = req.amount <= BigDecimal("1000000"), reference = UUID.randomUUID().toString())

2. Cambiá el puerto del provider-sim a 8082 en su application.yml:

server:
  port: 8082

3. Reiniciá el provider-sim:

cd provider-sim
./gradlew bootRun

4. Verificá que responde:

curl -X POST localhost:8082/v2/charges -H "Content-Type: application/json" -d '{"amount":4000}'

Deberías ver {"approved":true,"reference":"..."}.

¿Y el Proveedor A en el 8081?

Como ProviderBAdapter es @Primary, ahora solo se usa el Proveedor B. El cliente del Proveedor A sigue en el código, pero nadie lo llama. Por eso no importa que el 8081 ya no esté arriba. Ese es justo el punto: elegir el proveedor es una decisión de infraestructura.


Parte 3 — El momento "wow" (10 min)

Mirá lo que no cambiaste para cambiar de proveedor:

  • domain/ → intacto. El negocio no sabe que hubo un cambio.
  • application/ → intacto. El servicio sigue pidiéndole charge al puerto.
  • El test de la sesión 2 → intacto, y sigue en verde.

Corré los tests del negocio:

./gradlew test

Verdes. No los tocaste. Cambiaste de pasarela de pago sin abrir una sola clase de negocio.

Esto es lo que vale toda la charla

En la arquitectura vieja, cambiar de proveedor era una cirugía a corazón abierto sobre la lógica. Acá fue agregar una clase en un rincón de infraestructura. El dominio ni se enteró. Ese es el regalo de poner el negocio detrás de un puerto.

El orden para probarlo en vivo

  1. Levantá provider-sim en el puerto 8082: cd provider-sim && ./gradlew bootRun
  2. Levantá payments en el 8080: cd payments && ./gradlew bootRun
  3. Hacé el mismo POST http://localhost:8080/payments con {"amount":4000}. Funciona igual, pero ahora cobra por el Proveedor B. La API de entrada no cambió, el negocio no cambió, solo cambió quién cobra por detrás.
  4. Corré los tests: cd payments && ./gradlew test → todos verdes, sin tocar el dominio.

Parte 4 — La foto completa

[ POST /payments ]
ProcessPaymentService  ──charge()──►  PaymentGateway (puerto)
   (sin cambios)                          ├── ProviderAAdapter   (ayer)
                                          └── ProviderBAdapter   (hoy, @Primary)

Un puerto. Dos adaptadores. El negocio mira al puerto y nunca a los adaptadores. Esa es, en una imagen, la razón por la que elegimos hexagonal.


Cierre de la sesión

git add .
git commit -m "sesion-4: segundo proveedor sin tocar el negocio"
git branch sesion-4

En la Sesión 5 le ponemos cara: una interfaz en Vaadin, que es solo otro adaptador de entrada.