Skip to content
edgarrthPublic

About

PoC de integración de microservicios de pagos con gRPC Java y Spring gRPC: clientes simultáneos, cuatro modos RPC, mTLS, idempotencia y benchmark comparativo. Java 25, Spring Boot 4.1, PostgreSQL, DDD y arquitectura hexagonal

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Integración de pagos mediante gRPC

PoC de integración entre microservicios con Java 25, Spring Boot 4.1.1 y gRPC. Se expone una API REST de pagos y se delega la autorización a un adquirente independiente mediante HTTP/2 y Protocol Buffers. El foco técnico es el contrato gRPC y su comportamiento bajo concurrencia, errores, reintentos, streaming y conexiones autenticadas.

Se reproducen autorización, rechazo, consulta de decisiones, recepción de lotes y evaluación de riesgo en una conversación bidireccional. Los importes se expresan en unidades menores: 12500 representa S/ 125,00 cuando se usa PEN. Se usan tokens sintéticos; no se procesan números de tarjeta. Se aprueba hasta 500000 unidades menores y se rechaza un importe mayor o el token tok_decline. La evaluación de riesgo no registra autorizaciones.

Capacidades gRPC

Capacidad Implementación y propósito
Contrato primero Se generan mensajes y stubs desde contracts/src/main/proto/payments.proto. Se versiona el paquete como axiz.payments.v1; se reserva el campo 6.
RPC unaria Authorize: se registra una decisión durable y se devuelve su resultado.
Streaming de servidor History: se transmiten decisiones individualmente con isReady() y setOnReadyHandler().
Streaming de cliente SubmitBatch: se reciben hasta 100 pagos y se devuelve un resumen al cerrar el envío.
Streaming bidireccional Evaluate: se responde a cada pago durante la conversación, sin esperar a recibir el lote completo.
Control de flujo Se regula la recepción con request(1); se envía únicamente cuando el transporte está listo. Se mantiene como máximo una respuesta pendiente en el servidor dúplex.
Plazos Se exige un deadline en los métodos funcionales; se aplica un plazo total desde el cliente, incluyendo sus reintentos.
Cancelación Se interrumpe el transporte al cancelar o vencer el plazo. Se descarta un lote recibido si se cancela antes de comenzar su procesamiento.
Reintentos seguros Se permiten hasta tres intentos únicamente para Authorize y UNAVAILABLE, con espera exponencial. Se conserva el mismo identificador del pago.
Idempotencia Se usa una clave primaria en PostgreSQL y INSERT ... ON CONFLICT DO NOTHING, seguido de comparación del contenido dentro de una transacción. Se rechaza la reutilización de la clave con otro pago.
Transporte Se reutiliza un canal Netty shaded; se aplican compresión gzip, límites de mensajes y metadatos, keepalive sin llamadas ociosas y cierre ordenado.
Resolución y balanceo Se usa el resolver del canal y la política round_robin; con DNS que devuelve varias direcciones se distribuyen llamadas entre ellas. El despliegue incluido contiene una instancia del adquirente.
Seguridad Se exige mTLS con una CA local, validación del nombre del servidor y credencial Bearer mediante interceptor. También se autentica la API REST.
Correlación Se propaga x-request-id del ingreso REST al contexto y a los metadatos gRPC. Se devuelve en cabeceras o trailers.
Observabilidad Se registran duración, método y código gRPC en Micrometer, tanto en cliente como en servidor. Los identificadores y tokens no se usan como etiquetas.
Operación Se proporcionan el protocolo estándar de salud gRPC, reflection configurable y endpoints de Actuator. No se registran las consultas periódicas de salud.
Errores Se traducen códigos gRPC a HTTP y ProblemDetail; se evita devolver detalles internos de persistencia.
Pruebas Se usan conexiones TCP reales, handshakes mTLS, arranque de Spring y solicitudes HTTP. Se comprueban también cancelación, límites y concurrencia.

No se aplica reintento automático a streams. Se conserva la identidad de cada pago para que pueda recuperarse un lote con una nueva solicitud explícita. Un plazo vencido no acredita que la autorización haya sido revertida: se repite la solicitud con el mismo contenido e identificador para recuperar una decisión que pudiera haberse confirmado.

Arquitectura

flowchart TD
  C["Cliente REST o curl"] --> P["payment-service :8080"]
  P -->|"gRPC HTTP/2, mTLS y Protobuf"| A["acquirer-service :9090"]
  A -->|"JDBC y Flyway"| D["PostgreSQL :5432"]
  G["grpcurl"] -->|"mTLS"| A
Loading

Se separan los proyectos Maven de ambos servicios. contracts contiene únicamente el contrato y código generado. grpc-support contiene utilidades técnicas de metadatos y métricas; no comparte lógica de negocio ni persistencia.

Coexistencia de los clientes gRPC Java y Spring

Se mantienen dos canales activos en la misma instancia de payment-service, ambos conectados al único servidor Spring gRPC del adquirente. Se selecciona un cliente por solicitud mediante X-Grpc-Client: java o X-Grpc-Client: spring; no se reinicia el proceso ni se recrean contenedores para alternarlos. Cada solicitud ejecuta un solo RPC funcional con el cliente indicado.

Cliente Construcción y seguridad del canal Ciclo de vida
gRPC Java GrpcConfiguration construye NettyChannelBuilder y configura mTLS con GrpcSslContexts. RpcChannel administra el cierre del canal Java.
Spring gRPC SpringGrpcConfiguration usa GrpcChannelFactory, el canal lógico acquirer y un bundle SSL de Spring Boot. La fábrica administra el cierre del canal Spring.

Spring gRPC utiliza gRPC Java internamente. Se comparan la configuración y administración de los canales. Se crean dos adaptadores GrpcAcquirerAdapter independientes con los mismos stubs blocking y asíncronos. RoutingAcquirerAdapter implementa el puerto hexagonal y delega al adaptador seleccionado sin duplicar lógica funcional. Ambos clientes soportan los cuatro modos, deadlines, gzip, control de flujo, Bearer, correlación, métricas y reintento exclusivo de Authorize ante UNAVAILABLE. Se reutiliza GrpcChannelPolicy para aplicar los mismos límites y backoff.

RequestFilter valida la selección, la guarda en un contexto acotado a la solicitud y devuelve el cliente utilizado en la cabecera X-Grpc-Client. El contexto se libera en finally; las solicitudes concurrentes no comparten una selección mutable. Un valor de cabecera diferente de java o spring devuelve HTTP 400 antes de invocar gRPC. Si se omite la cabecera, se utiliza el cliente predeterminado de .env; esa configuración no desactiva el otro canal. La selección no cambia las credenciales ni las reglas de autorización.

Los dos canales usan la misma CA y la identidad cliente del despliegue. El bundle PEM de Spring se registra cuando TLS está habilitado; ambos canales admiten el diagnóstico local sin TLS. El servidor permanece exclusivamente con Spring gRPC. Se siguen las APIs oficiales de GrpcChannelFactory y cliente Spring Boot con SSL bundles.

Se consulta GET /actuator/info para comprobar que los dos clientes están disponibles:

{"grpcClients":["java","spring"],"grpcClientDefault":"java"}

Se distinguen los RPC de cada canal en las métricas mediante la etiqueta client. La idempotencia se mantiene en el adquirente y PostgreSQL: la misma clave y contenido enviados desde distintos clientes devuelven la misma decisión; un contenido diferente devuelve conflicto.

Tecnologías y versiones

Tecnología Versión Uso
Java 25; validación con Temurin 25.0.4.1 Runtime, compilación y tipado de ambos servicios.
Maven 3.10.0 Reactor, generación de contratos, pruebas y empaquetado.
Spring Boot 4.1.1 Configuración, REST, Actuator y soporte oficial de cliente y servidor gRPC.
Spring Framework 7.0.9 Versión administrada por Spring Boot; inyección, validación y transacciones.
Spring gRPC 1.1.1 Integración del servidor y de la alternativa cliente con el ciclo de vida de Spring.
gRPC Java 1.83.1 Stubs, streams y transporte Netty shaded.
Protocol Buffers 4.35.1 Mensajes binarios y generación mediante protoc.
PostgreSQL 18.6 Persistencia transaccional e idempotencia concurrente.
Flyway 12.4.0 Migración del esquema durante el arranque del adquirente.
Spotless y Google Java Format 3.6.0 y 1.34.1 Formato del código Java, verificado en mvn verify.

Estructura del proyecto

Carpeta Contenido
payment-service/ Proyecto de API REST, casos de uso y cliente gRPC.
acquirer-service/ Proyecto del adquirente gRPC, dominio y adaptador JDBC.
contracts/ Contratos versionados y generación de mensajes y stubs.
grpc-support/ Interceptores y claves de contexto compartidas.
infrastructura/ Compose, Dockerfiles, generación de certificados y ejemplos de request/response.
datasets/ Pagos sintéticos y carga idempotente a través de la API.
scripts/ Compilación, arranque en host, rutas y pruebas reproducibles.
verification/ Salida de compilación y reportes de las pruebas ejecutadas.

Código principal y semántica

Clase Responsabilidad
PaymentController Se valida el ingreso REST y se ejecuta el caso de uso correspondiente.
PaymentService Se aplica el límite de lote y se delega por el puerto del adquirente.
GrpcAcquirerAdapter Se convierten modelos a Protobuf, se aplican deadlines y se coordinan streams con control de flujo.
GrpcConfiguration Se construye y administra un canal compartido con mTLS, retry, balanceo, compresión y límites.
GrpcConfiguration / SpringGrpcConfiguration Se crean simultáneamente el canal Java y el canal administrado por Spring.
GrpcClientAdapters / RoutingAcquirerAdapter Se crean ambos adaptadores y se selecciona uno por solicitud en el puerto hexagonal.
GrpcChannelPolicy / GrpcClientInfo Se unifica la política de transporte y se informan ambos clientes disponibles y el predeterminado.
RequestFilter / ClientHeaders Se autentica el ingreso y se propaga la correlación al RPC.
RpcGuard / ClientMetrics Se validan metadatos del servidor y se registran métricas del transporte.
AcquirerGrpcAdapter Se implementan las cuatro modalidades del contrato y se traducen errores de aplicación.
PaymentUseCase / Authorization Se ejecutan autorización y evaluación; se aplica la política de riesgo sintética.
JdbcAuthorizationStore Se registra o recupera una decisión sin duplicarla, incluso bajo solicitudes concurrentes.
ApiErrors Se transforman fallos gRPC a respuestas REST estructuradas.

Se valida todo el lote recibido antes de escribir. El lote no constituye una transacción única: cada pago se confirma de forma independiente. Si aparece un conflicto posterior, los pagos previos pueden permanecer confirmados. Se recupera el resultado repitiendo los pagos con sus mismas claves; no se presupone entrega exactamente una vez por parte del transporte.

Rutas y orden funcional

Paso Endpoint REST Función RPC o mecanismo técnico
Comprobación GET :8080/actuator/health Se comprueba la API de pagos. Salud HTTP.
Comprobación GET :8081/actuator/health Se comprueba el adquirente y su base. Salud HTTP y JDBC.
Evaluación opcional POST :8080/api/payments/evaluate Se evalúan hasta 100 pagos sin autorizarlos. Evaluate, streaming bidireccional.
Autorización POST :8080/api/payments Se obtiene una decisión durable para un pago. Authorize, RPC unaria idempotente.
Autorización por lote POST :8080/api/payments/batch Se autorizan hasta 100 pagos. SubmitBatch, streaming de cliente.
Consulta GET :8080/api/payments?merchantId=...&limit=50 Se recuperan decisiones anteriores. History, streaming de servidor.
Inspección GET :8080/actuator/metrics/axiz.grpc.client Se consultan duración y cantidad de RPC del cliente. Micrometer; aparece después de realizar llamadas.
Inspección GET :8081/actuator/metrics/axiz.grpc.server Se consultan métricas de los RPC funcionales. Micrometer.
Operación GET :8080/actuator/info, GET :8081/actuator/info Se consultan información del proceso y disponibilidad de clientes. Actuator; en payment se informan grpcClients y grpcClientDefault y en acquirer se conserva la respuesta sin información adicional.
Operación GET :8080/actuator/metrics, GET :8081/actuator/metrics Se enumeran métricas. Actuator.
Código gRPC HTTP Interpretación
INVALID_ARGUMENT 400 Datos o metadatos inválidos.
UNAUTHENTICATED / PERMISSION_DENIED 401 / 403 Credencial ausente, inválida o acceso rechazado.
NOT_FOUND 404 Recurso no encontrado.
ALREADY_EXISTS / FAILED_PRECONDITION 409 Conflicto de idempotencia o estado.
RESOURCE_EXHAUSTED 429 Límite del transporte excedido.
CANCELLED 408 RPC cancelada.
UNAVAILABLE / DEADLINE_EXCEEDED 503 / 504 Integración no disponible o plazo agotado.
Otros fallos gRPC 502 Error de integración.

Un rechazo de riesgo devuelve HTTP 200 y DECLINED: se completó correctamente la evaluación funcional.

Preparación

cp .env.example .env

Infraestructura con Docker y aplicaciones en el host

En una terminal se mantiene PostgreSQL en primer plano:

docker compose --env-file .env -f infrastructura/docker-compose.yml up postgres

En otra terminal se compilan y arrancan ambos servicios, se generan los certificados y se carga el dataset:

bash scripts/run-host.sh

Se conserva el control de ambos procesos y se cierran al interrumpir el script. La generación de certificados y la carga pueden repetirse. El arranque del adquirente aplica Flyway antes de atender solicitudes. No se ejecuta una segunda inicialización SQL en Compose.

Para depurar desde un IDE, se ejecuta primero mvn install en la raíz y se importan los proyectos Maven. Se generan los certificados con bash infrastructura/generate-certs.sh y se exporta el archivo de configuración:

set -a
source .env
set +a
mvn -B install
bash infrastructura/generate-certs.sh
bash scripts/routes.sh

Se arrancan las clases pe.axiz.acquirer.Application y pe.axiz.payment.Application con directorio de trabajo en la raíz. Después se ejecuta bash datasets/seed.sh. No se mezcla este modo con aplicaciones ya levantadas por Compose, pues se usan los mismos puertos.

Todo con Docker Compose

Se construye primero la imagen base de Java y Maven, con dependencias y herramientas resueltas. Se usan additional_contexts para que BuildKit prepare esa base antes de compilar cada aplicación. Se conserva la base con una etiqueta estable y una capa de dependencias que solo cambia al modificar los POM o el Dockerfile base. Al modificar código se recompilan las aplicaciones sin reinstalar sus herramientas. La imagen de ejecución se reutiliza y el contenedor corre como usuario sin privilegios.

Se construye y levanta todo con un comando, en primer plano:

docker compose --env-file .env -f infrastructura/docker-compose.yml --profile app up --build

Se ejecutan certificados, migración Flyway, ambos servicios y carga inicial. El contenedor seed debe terminar con código 0; al finalizar la carga se imprimen las rutas disponibles. Los servicios de larga duración permanecen en primer plano. Los healthchecks redirigen su salida y no se registran como accesos de aplicación.

En las siguientes ejecuciones se usan las imágenes existentes, sin construirlas:

docker compose --env-file .env -f infrastructura/docker-compose.yml --profile app up --no-build

La primera ejecución requiere acceso a los registros de contenedores y a Maven Central. Se mantienen datos y certificados en volúmenes. Se detiene y retira el despliegue sin eliminar esos datos con:

docker compose --env-file .env -f infrastructura/docker-compose.yml --profile app down

Pruebas de ambos clientes sin reinicios

Se reconstruye la versión actualizada una vez con el comando Compose de la sección anterior. Después se pueden usar ambos clientes desde la misma instancia. También se aplica esta selección cuando los servicios se ejecutan en el host.

Se autoriza un pago primero con Java y después con Spring. La segunda solicitud comprueba la idempotencia compartida:

set -a
source .env
set +a
for client in java spring; do
  curl -i -sS http://localhost:8080/api/payments \
    -H "Authorization: Bearer $API_TOKEN" \
    -H "X-Grpc-Client: $client" \
    -H 'Content-Type: application/json' \
    -d '{"paymentId":"dual-client-demo","merchantId":"merchant_demo","amountMinor":12500,"currency":"PEN","token":"tok_demo"}'
done

Se espera HTTP 200 y la misma decisión APPROVED en ambas llamadas. Se devuelve X-Grpc-Client: java en la primera y X-Grpc-Client: spring en la segunda. Las cabeceras también se admiten en historial, lote y evaluación. Todos los ejemplos curl posteriores pueden ejecutarse con cualquiera de las dos cabeceras.

Se prueba la coexistencia con un solo comando, sin Maven, grpcurl ni operaciones Docker:

bash scripts/test-clients.sh

Se ejecuta la suite funcional completa con cada cliente. Después se envían 24 solicitudes concurrentes alternando Java y Spring sobre la misma clave, se comprueban replay y conflicto entre clientes, se rechaza una selección inválida y se verifica un único registro persistente desde ambos historiales. Se requieren curl, Python 3 y el despliegue actualizado en ejecución. Después se ejecuta el reporte de rendimiento de la sección siguiente. Se conserva scripts/test-clients-compose.sh como entrada compatible para el mismo comando; ya no recrea contenedores ni restaura un cliente al terminar.

Para probar una sola alternativa sobre el despliegue existente:

bash scripts/test-live.sh java
bash scripts/test-live.sh spring

Sin argumento, scripts/test-live.sh prueba el cliente predeterminado. scripts/grpc-test.sh inspecciona directamente el adquirente; no pasa por ninguno de los clientes REST.

Reporte de latencia y rendimiento por cliente

scripts/test-clients.sh genera automáticamente el reporte después de comprobar el funcionamiento y la coexistencia. También se puede ejecutar únicamente la medición sobre los servicios ya desplegados:

bash scripts/benchmark.sh

Se imprime una tabla por cliente y operación, el tiempo acumulado de los bloques medidos y la diferencia porcentual de Spring respecto de Java. Se guardan archivos reports/grpc-clients-<fecha-UTC>.json y .csv con un nombre único por ejecución. JSON conserva la configuración, los resultados, los códigos RPC, aprobaciones, rechazos funcionales, categorías de error y comparaciones; CSV permite analizar las filas en una hoja de cálculo. No se incluyen credenciales ni datos de tarjetas. Las opciones y sus límites se describen exclusivamente en .env.example; si se conserva un .env anterior, se aplican los valores predeterminados de las opciones nuevas.

Escenario Operación gRPC Trabajo medido
authorize Unary Authorize Se autorizan pagos con claves nuevas; incluye escritura en PostgreSQL.
replay Unary Authorize Se repite un pago precargado; se mide el recorrido idempotente.
history Server streaming History Se recibe un historial precargado de tamaño fijo.
batch Client streaming SubmitBatch Se envía un lote de tamaño fijo con claves nuevas y se reciben sus decisiones.
evaluate Bidirectional streaming Evaluate Se evalúa un conjunto del mismo tamaño que el lote sin persistir autorizaciones.
Métrica Interpretación
Tiempo acumulado por cliente Suma de la duración real de sus bloques medidos; se excluyen preparación, calentamiento, consulta de métricas y espera entre bloques.
Media, mínimo, máximo y desviación estándar Latencia HTTP observada, incluida la serialización y lectura de la respuesta. Se conservan también los tiempos de intentos fallidos.
p50, p95 y p99 Percentiles calculados por interpolación sobre las muestras del cliente y escenario. Se informa el tamaño de muestra para interpretar la cola de latencia.
Solicitudes/s Intentos divididos entre la duración real de los bloques; incluye concurrencia.
Solicitudes exitosas/s Respuestas válidas divididas entre la duración de los bloques.
Unidades/s Pagos procesados por segundo; en historial, decisiones recibidas por segundo. Un lote representa varias unidades y un solo RPC.
Errores y porcentaje Fallos HTTP, transporte o contrato de respuesta. No se confunden errores técnicos con rechazos funcionales de pagos.
RPC: cantidad y media Diferencia de COUNT y TOTAL_TIME de Micrometer antes y después de medir; se filtra por cliente y método. Se excluye el calentamiento.
Códigos RPC Cantidad de llamadas terminadas por código gRPC durante la ventana medida.
Diferencia Spring/Java Variación porcentual por escenario. En latencia, un valor positivo representa mayor tiempo; en rendimiento representa más solicitudes exitosas/s.

Se ejecutan primero solicitudes de calentamiento para estabilizar canales y conexiones. Se distribuye la misma cantidad de muestras entre bloques y se alterna su orden con una semilla reproducible. Cada bloque utiliza un número fijo de trabajadores; se mide la latencia desde que un trabajador inicia la solicitud, sin incluir su espera en la cola. El rendimiento utiliza el tiempo real del bloque, no la suma de latencias concurrentes.

La latencia REST incluye HTTP, validación, mapeo, gRPC y el trabajo del adquirente. El temporizador RPC mide una llamada lógica desde el interceptor cliente hasta su cierre, incluidos reintentos; no cuenta cada intento interno por separado. Los canales gRPC se reutilizan, mientras el medidor HTTP abre conexiones mediante la biblioteca estándar de Python. No se atribuye toda la diferencia observada a Spring: ambos clientes comparten JVM, servidor, base de datos y recursos. Los percentiles de muestras pequeñas son exploratorios; se repite la medición antes de establecer conclusiones de rendimiento.

Se utilizan identificadores únicos para evitar replays accidentales en las operaciones de escritura. Se precargan solo el pago de replay y el historial del benchmark mediante la API. Los pagos de autorización y lote quedan persistidos para su revisión; no se eliminan registros ajenos. Se ejecuta la comparación sin tráfico adicional ni reinicios durante la ventana. Si los contadores RPC no coinciden con los intentos HTTP, se marca la comparación como inválida, se conserva el reporte y el script termina con código distinto de cero. También se devuelve un código de fallo si existen errores técnicos en las muestras.

Se verifican los cálculos y la exportación del reporte con pruebas Python sobre un servidor HTTP controlado:

python3 -m unittest discover -s scripts/tests -v

Estas pruebas comprueban percentiles, rendimiento exitoso, separación de rechazos funcionales y errores técnicos, reinicios de contadores, validación de configuración y generación de JSON/CSV sin incluir secretos. Se ejecutan también dentro de scripts/test-all.sh.

Carga de datos

Se incluye datasets/payments.json con una aprobación y un rechazo. Se carga mediante el RPC de lote, a través de REST, para probar el mismo camino que se usa con las solicitudes funcionales. Flyway crea exclusivamente el esquema; el dataset no duplica las migraciones.

set -a
source .env
set +a
bash datasets/seed.sh

Se espera una lista con seed-approved / APPROVED y seed-declined / DECLINED. La repetición devuelve las decisiones originales y no crea filas adicionales.

Pruebas por curl

Se mantienen ambos servicios en ejecución y se carga .env en la terminal de pruebas:

set -a
source .env
set +a
curl -fsS http://localhost:8080/actuator/health
curl -fsS http://localhost:8081/actuator/health

Se espera {"status":"UP",...} en ambos casos.

Autorización unaria e idempotencia

Se autoriza S/ 125,00 y se verifica la traducción REST → Protobuf → JDBC. El request se conserva en infrastructura/requests/authorize.json.

curl --fail-with-body -sS http://localhost:8080/api/payments \
  -H "Authorization: Bearer $API_TOKEN" \
  -H 'X-Request-Id: prueba-unaria' \
  -H 'Content-Type: application/json' \
  --data-binary @infrastructura/requests/authorize.json

Respuesta esperada:

{"paymentId":"pay-demo","status":"APPROVED","reason":"AUTHORIZED","amountMinor":12500,"currency":"PEN"}

Se repite el mismo comando para comprobar el replay sin duplicación. Se cambia el importe manteniendo la clave para comprobar el conflicto:

curl -sS -i http://localhost:8080/api/payments \
  -H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
  --data '{"paymentId":"pay-demo","merchantId":"merchant_demo","amountMinor":12501,"currency":"PEN","token":"tok_demo"}'

Se espera HTTP 409 y un cuerpo ProblemDetail con grpcStatus: "ALREADY_EXISTS".

Rechazo funcional

Se envía un pago que excede el límite sintético de riesgo:

curl --fail-with-body -sS http://localhost:8080/api/payments \
  -H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
  --data '{"paymentId":"pay-declined","merchantId":"merchant_demo","amountMinor":999999,"currency":"PEN","token":"tok_demo"}'

Respuesta esperada:

{"paymentId":"pay-declined","status":"DECLINED","reason":"RISK_POLICY","amountMinor":999999,"currency":"PEN"}

Streaming de cliente

Se envían los dos pagos del dataset como mensajes gRPC independientes y se obtiene el resumen al cerrar el envío:

curl --fail-with-body -sS http://localhost:8080/api/payments/batch \
  -H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
  --data-binary @datasets/payments.json

Respuesta esperada:

[{"paymentId":"seed-approved","status":"APPROVED","reason":"AUTHORIZED","amountMinor":12500,"currency":"PEN"},{"paymentId":"seed-declined","status":"DECLINED","reason":"RISK_POLICY","amountMinor":999999,"currency":"PEN"}]

Streaming bidireccional

Se usan identificadores nuevos para comprobar evaluaciones sin persistencia. Se alternan mensajes y respuestas en el enlace gRPC:

curl --fail-with-body -sS http://localhost:8080/api/payments/evaluate \
  -H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
  --data '{"payments":[{"paymentId":"eval-only","merchantId":"merchant_demo","amountMinor":12500,"currency":"PEN","token":"tok_demo"}]}'

Respuesta esperada:

[{"paymentId":"eval-only","status":"APPROVED","reason":"AUTHORIZED","amountMinor":12500,"currency":"PEN"}]

Streaming de servidor

Se recupera el historial del comercio. Se comprueba que aparecen las autorizaciones, pero no eval-only:

curl --fail-with-body -sS 'http://localhost:8080/api/payments?merchantId=merchant_demo&limit=50' \
  -H "Authorization: Bearer $API_TOKEN"

Se espera una lista de decisiones con seed-approved, seed-declined y los pagos autorizados durante las pruebas. El orden corresponde al momento en que se registraron. limit=0 devuelve HTTP 400.

Validación y autenticación

Se envía un importe inválido y se realiza una solicitud sin credencial:

curl -sS -i http://localhost:8080/api/payments \
  -H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
  --data '{"paymentId":"invalid","merchantId":"merchant_demo","amountMinor":0,"currency":"PEN","token":"tok_demo"}'
curl -sS -i 'http://localhost:8080/api/payments?merchantId=merchant_demo'

Se esperan HTTP 400 y HTTP 401 respectivamente. No se registran decisiones para esas solicitudes.

Métricas del transporte

Después de las llamadas anteriores se inspeccionan duración, cantidad, método y estado:

curl -fsS http://localhost:8080/actuator/metrics/axiz.grpc.client
curl -fsS http://localhost:8081/actuator/metrics/axiz.grpc.server

Se espera un objeto de métricas con COUNT, TOTAL_TIME, MAX y etiquetas disponibles de método y estado. No se imprimen tokens ni identificadores de pago en estas métricas.

Inspección directa con grpcurl

Se requiere grpcurl instalado y los certificados cliente disponibles en el host.

Se inspecciona reflection, se autoriza un pago, se transmite el historial, se envía un lote, se intercambian evaluaciones y se consulta salud. Se usan certificados, credencial y deadline en cada llamada. En el formato JSON de Protobuf, int64 aparece como cadena; por eso el script usa "amountMinor":"12500".

En el modo Compose, se copian los certificados necesarios para la inspección desde el volumen del adquirente:

mkdir -p infrastructura/certs
docker compose --env-file .env -f infrastructura/docker-compose.yml --profile app cp acquirer:/certs/ca.crt infrastructura/certs/ca.crt
docker compose --env-file .env -f infrastructura/docker-compose.yml --profile app cp acquirer:/certs/client.crt infrastructura/certs/client.crt
docker compose --env-file .env -f infrastructura/docker-compose.yml --profile app cp acquirer:/certs/client.key infrastructura/certs/client.key
chmod 600 infrastructura/certs/client.key
bash scripts/grpc-test.sh

Se observa el intercambio dúplex interactivo sustituyendo la entrada printf del script por líneas JSON introducidas en stdin de grpcurl -d @; se recibe una respuesta por cada mensaje antes del cierre del envío.

Suite completa y compilación

Para probar únicamente los servicios desplegados, sin compilar ni descargar dependencias Java en el host, se ejecuta bash scripts/test-live.sh. Para probar ambos clientes y su coexistencia se utiliza bash scripts/test-clients.sh. La compilación y las pruebas Java utilizan Maven y su caché del host, independiente de la caché de la imagen base.

Con los servicios en ejecución, se ejecutan todas las pruebas Java y las suites funcionales de ambos clientes, incluida la concurrencia mixta, con un único comando:

bash scripts/test-all.sh

Se comprueban autorización, rechazo, replay, conflicto, credenciales, validación, 24 solicitudes concurrentes sobre la misma clave, lote, evaluación sin escritura e historial. Se generan identificadores únicos en cada ejecución para evitar contaminación entre pruebas. El recorrido funcional exige PostgreSQL real y los servicios levantados.

Para compilar, verificar formato y ejecutar las pruebas de transporte y Spring sin levantar la infraestructura:

bash scripts/build.sh

Se generan acquirer-service/target/acquirer-service-1.0.0.jar y payment-service/target/payment-service-1.0.0.jar. Se incluyen pruebas de ambos clientes, TCP real y mTLS, generación de certificados locales, límites de flujo y lote, plazo agotado, cancelación, reintento seguro, exclusión de reintentos para errores de validación, concurrencia, traducción REST y arranque del servidor gRPC de Spring. Para aplicar el formato antes de la verificación se usa mvn spotless:apply.

About

PoC de integración de microservicios de pagos con gRPC Java y Spring gRPC: clientes simultáneos, cuatro modos RPC, mTLS, idempotencia y benchmark comparativo. Java 25, Spring Boot 4.1, PostgreSQL, DDD y arquitectura hexagonal

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages