Enrique Valdivia Rios Portafolio - Senior Backend Engineer | Java · Spring Boot · Kubernetes | Fintech & Payments | Scalable Distributed Systems

Payment Gateway API: Construyendo una Pasarela de Pagos Profesional

Uno de los mayores retos en el desarrollo backend fintech es implementar un sistema de pagos que sea seguro, escalable y fácil de mantener. En este proyecto construí una API de pasarela de pagos desde cero usando Spring Boot 3.2, aplicando patrones que uso día a día en producción.

El código completo está en mi repositorio de GitHub.


El Problema

Las plataformas e-commerce necesitan procesar pagos con tarjeta de forma confiable. Esto implica:


Arquitectura

El proyecto sigue una arquitectura en capas con separación clara de responsabilidades:

Controller (DTOs) → Service (lógica) → Repository (persistencia)

                   CardProcessor (abstracción externa)

Stack Tecnológico

CapaTecnología
FrameworkSpring Boot 3.2
Base de datosPostgreSQL 15
CacheRedis 7
MigracionesFlyway
DocumentaciónOpenAPI 3.0 / Swagger UI
TestingJUnit 5, Mockito, Testcontainers
ContenedoresDocker / Docker Compose

Decisiones de Diseño

1. Abstracción del Procesador de Tarjetas

En lugar de acoplar la lógica de pagos a un PSP específico, creé una interfaz CardProcessor que cualquier implementación puede satisfacer:

public interface CardProcessor {
    ProcessorResult authorize(Payment payment);
    ProcessorResult capture(Payment payment);
    ProcessorResult refund(Payment payment);
    ProcessorResult reverse(Payment payment);
}

Para desarrollo y testing, incluí un SimulatedCardProcessor que sigue convenciones estándar de tarjetas de prueba:

// Tarjetas que terminan en "0002" → insufficient_funds
// Tarjetas que terminan en "0003" → processor_error
// Cualquier otra → aprobada

Esto permite correr la aplicación completa sin depender de un sandbox externo.

2. DTOs y Separación de Contratos

El controller nunca expone la entidad JPA directamente. Usa records inmutables:

public record PaymentRequest(
    @NotNull @Min(1) Long amount,
    @NotBlank @Size(min = 3, max = 3) String currency,
    @Size(min = 4, max = 4) String cardLastFour
) {}

public record PaymentResponse(
    UUID id, String merchantId, Long amount,
    String currency, PaymentStatus status,
    String processorRef, Instant createdAt
) {
    public static PaymentResponse from(Payment payment) { ... }
}

3. Error Handling con RFC 7807

Todas las respuestas de error usan ProblemDetail (estándar RFC 7807), lo que hace que los clientes puedan parsear errores de forma consistente:

@ExceptionHandler(PaymentNotFoundException.class)
public ProblemDetail handleNotFound(PaymentNotFoundException ex) {
    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.NOT_FOUND, ex.getMessage());
    problem.setTitle("Payment Not Found");
    return problem;
}

4. Idempotencia

El header Idempotency-Key previene cobros duplicados. Si un request llega con una key que ya existe, se retorna el payment original sin procesarlo de nuevo:

if (payment.getIdempotencyKey() != null) {
    Optional<Payment> existing = paymentRepository
        .findByIdempotencyKey(payment.getIdempotencyKey());
    if (existing.isPresent()) {
        return existing.get(); // retorna sin re-procesar
    }
}

Ciclo de Vida del Pago

El flujo completo de un pago sigue este diagrama de estados:

PENDING → AUTHORIZED → CAPTURED → REFUNDED

          REVERSED

           FAILED

Cada transición valida el estado actual antes de proceder:

if (payment.getStatus() != PaymentStatus.AUTHORIZED) {
    throw new PaymentStateException(
        payment.getStatus(), PaymentStatus.AUTHORIZED);
}

API Endpoints

MétodoEndpointDescripción
POST/api/v1/paymentsCrear y autorizar un pago
GET/api/v1/payments/{id}Consultar estado del pago
POST/api/v1/payments/{id}/captureCapturar pago autorizado
POST/api/v1/payments/{id}/refundReembolsar pago capturado
POST/api/v1/payments/{id}/reverseReversar autorización

Ejemplo de uso

# Crear un pago
curl -X POST http://localhost:8080/api/v1/payments \
  -H "Content-Type: application/json" \
  -H "X-Merchant-Id: merchant_001" \
  -H "Idempotency-Key: order-12345" \
  -d '{"amount": 2500, "currency": "USD", "cardLastFour": "4242"}'

# Capturar el pago
curl -X POST http://localhost:8080/api/v1/payments/{id}/capture \
  -H "X-Merchant-Id: merchant_001"

Testing

El proyecto tiene 33 tests organizados en cuatro niveles:

@Test
void shouldFailWhenDeclined() {
    when(cardProcessor.authorize(any()))
        .thenReturn(ProcessorResult.declined("insufficient_funds"));

    Payment result = paymentService.createPayment(payment);

    assertEquals(PaymentStatus.FAILED, result.getStatus());
}

Adicionalmente hay un test de integración con Testcontainers que levanta un PostgreSQL real para validar el repository y las migraciones Flyway.


Cómo Ejecutar

git clone https://github.com/enriquevaldivia1988/payment-gateway-api.git
cd payment-gateway-api

# Levantar infraestructura
docker-compose up -d

# O para desarrollo local
docker-compose up -d postgres redis
./mvnw spring-boot:run

# Ejecutar tests
./mvnw test

Swagger UI disponible en http://localhost:8080/swagger-ui.html.


Conclusión

Este proyecto demuestra cómo estructurar una API de pagos lista para producción: con separación de capas, abstracción de dependencias externas, manejo robusto de errores, y una suite de tests que cubre tanto lógica de negocio como integración con la base de datos.


Enrique Valdivia

Ver código en GitHub