Overview

This project implements a Payment Gateway API that allows merchants to process card payments and retrieve payment details. The gateway validates incoming payment requests, forwards valid payments to an acquiring bank simulator, persists outcomes in PostgreSQL, and supports production-oriented controls (security, idempotency, rate limiting, resilience, and observability).

Architecture

┌──────────┐     ┌───────────────────┐     ┌────────────────┐
│ Merchant │────▶│  Payment Gateway  │────▶│ Acquiring Bank  │
│          │◀────│  (Spring Boot)    │◀────│ (Simulator)     │
└──────────┘     └───────────────────┘     └────────────────┘
                         │
          ┌──────────────┼──────────────┐
          ▼              ▼              ▼
   ┌────────────┐ ┌────────────┐ ┌─────────────┐
   │ PostgreSQL │ │ Idempotency│ │ Resilience4j│
   │ (payments)│ │   store    │ │ retry + CB  │
   └────────────┘ └────────────┘ └─────────────┘

Component Responsibilities

Component Responsibility
PaymentGatewayController Versioned REST API (/api/v1). Optional Idempotency-Key header.
PaymentGatewayService Validation, idempotency replay, bank orchestration, persistence.
BankClient RestTemplate call to the bank with Resilience4j retry and circuit breaker.
PaymentJpaRepository JPA persistence for payments.
IdempotencyJpaRepository Stores cached HTTP status + JSON for idempotent POSTs (validation outcomes and successful bank outcomes).
ApiKeyAuthenticationFilter Validates X-API-Key, assigns ROLE_MERCHANT or ROLE_ADMIN.
RateLimitingFilter Token bucket (Bucket4j) per API key, registered after API key auth in the security chain.
CommonExceptionHandler Maps domain and infrastructure exceptions to HTTP responses.

API Endpoints (v1)

All merchant routes require header X-API-Key. Swagger/OpenAPI documents the scheme.

POST /api/v1/payments — Process a Payment

Optional header: Idempotency-Key (recommended for safe retries). Same key + same request fingerprint replays the stored response. Same key + different body returns 409 Conflict.

Request Body:

{
  "card_number": "2222405343248877",
  "expiry_month": 4,
  "expiry_year": 2030,
  "currency": "GBP",
  "amount": 100,
  "cvv": "123"
}

Responses:

Status Condition Body
200 OK Payment authorized by bank { "id": "uuid", "status": "Authorized", "cardNumberLastFour": 8877, ... }
200 OK Payment declined by bank Same shape with "status": "Declined"
400 Bad Request Validation failure { "status": "Rejected", "errors": [...] }
409 Conflict Idempotency key reused with different payload { "message": "..." }
429 Too Many Requests Rate limit exceeded (when rate limiting is enabled) { "message": "..." }
502 Bad Gateway Bank unreachable / hard failure after retries { "message": "..." }
503 Service Unavailable Circuit breaker open for bank client { "message": "..." }

GET /api/v1/payments/{id} — Retrieve Payment Details

Status Condition
200 OK Payment found
404 Not Found Unknown id

Validation Rules

Validation runs before the bank is called. Currency is checked against ISO 4217 via java.util.Currency, then restricted to USD, GBP, EUR (assessment limit of three codes).

Production Readiness (Implemented)

A. Persistent storage

  • PostgreSQL with Spring Data JPA (PaymentEntity).
  • Flyway migration V1__create_payments_and_idempotency.sql creates payments and idempotency_requests.
  • Default datasource is configurable via SPRING_DATASOURCE_* (see application.yml). Docker Compose includes a postgres service for local development.

B. Idempotency

  • Optional Idempotency-Key header on POST.
  • Request fingerprint (SHA-256 of a canonical JSON map of request fields) prevents accidental key reuse with a different payload (409).
  • Successful bank outcomes and validation rejections (HTTP 400) are stored for replay. 502 paths are not cached so clients can retry after upstream failures.

C. Authentication and authorisation

  • API keys via X-API-Key.
  • gateway.security.merchant-api-keysROLE_MERCHANT.
  • gateway.security.admin-api-keysROLE_ADMIN + ROLE_MERCHANT.
  • Method security: @PreAuthorize("hasAnyRole('MERCHANT','ADMIN')") on payment endpoints.
  • gateway.security.enabled=false (used by the test profile) disables API key enforcement and @EnableMethodSecurity for slice/unit tests.

D. Rate limiting

  • Bucket4j greedy refill bucket per API key (defaults: 120 requests / minute, configurable).
  • gateway.ratelimit.enabled=false in integration tests and available for operators.

E. Encryption and safe handling

  • No logging of card numbers or CVV in BankClient (only URL and HTTP status).
  • bank.simulator.url defaults to http://localhost:8080 for the simulator; the prod profile default is https://localhost:8080 as a reminder to use TLS to real banks.
  • PCI: this remains a sample; production requires tokenization/Vault, network controls, and formal PCI scope reduction.

F. Health checks and monitoring

  • Spring Boot Actuator: health, info, metrics, prometheus exposed (tune for your environment).
  • Micrometer Prometheus registry on the classpath.
  • logback-spring.xml: JSON console logging on the prod profile via Logstash encoder.

G. Resilience

  • Resilience4j @Retry and @CircuitBreaker on BankClient.processPayment.
  • Retries focus on transient network types (ResourceAccessException, SocketTimeoutException).
  • CallNotPermittedException → HTTP 503 with a clear message.

H. Configuration management

  • Environment variables for datasource, bank URL, API keys, rate limit, and toggles (see application.yml).
  • Profiles: default (dev-friendly), test (H2, security off), prod (HTTPS bank default, Swagger off), integration (used by Testcontainers test).

I. API versioning

  • Base path /api/v1.

J. Concurrency

  • Replaced in-memory HashMap with database-backed storage and JPA.

K. Testing

  • Unit tests for service and WebMvc controller (security filters disabled for @WebMvcTest).
  • BankSimulatorContractTest: WireMock verifies the JSON contract sent to /payments (card_number, expiry_date MM/yyyy, currency, amount, cvv).
  • PaymentGatewayIntegrationTest: Testcontainers PostgreSQL, WireMock as the bank, full Spring context, Flyway, API key, and idempotency header. Skipped automatically when Docker is unavailable (@Testcontainers(disabledWithoutDocker = true)).
  • PaymentGatewayApplicationTest: loads the context with the test profile (H2).

Running Locally

docker-compose up -d postgres bank_simulator
export SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/payment_gateway
export SPRING_DATASOURCE_USERNAME=payment
export SPRING_DATASOURCE_PASSWORD=payment
./gradlew bootRun
  • API: http://localhost:8090/api/v1
  • Swagger (non-prod defaults): http://localhost:8090/swagger-ui/index.html
  • Prometheus scrape path: http://localhost:8090/actuator/prometheus (requires admin API key with current security rules)

Test Commands

./gradlew test

Integration tests require Docker. Without Docker, the Testcontainers-based test is skipped.

Project Structure (high level)

src/main/java/com/checkout/payment/gateway/
  client/BankClient.java
  configuration/{ApplicationConfiguration,SecurityConfiguration,MethodSecurityConfiguration,OpenApiConfiguration}.java
  controller/PaymentGatewayController.java
  entity/{PaymentEntity,IdempotencyEntity}.java
  repository/{PaymentJpaRepository,IdempotencyJpaRepository}.java
  security/{ApiKeyAuthenticationFilter,RateLimitingFilter,RequestFingerprinter}.java
  service/PaymentGatewayService.java
src/main/resources/
  application.yml
  logback-spring.xml
  db/migration/V1__create_payments_and_idempotency.sql
src/test/java/.../integration/PaymentGatewayIntegrationTest.java
src/test/java/.../client/BankSimulatorContractTest.java

Notes and Follow-Ups

  • Secrets: API keys are comma-separated lists in configuration; production should use a secret manager and short-lived credentials.
  • Observability: add RED metrics, trace IDs, and business metrics (payments_authorized_total, latency histograms) as next steps.
  • Gateway vs embedded filters: for very high traffic, move rate limiting and auth to an edge gateway (Kong, Envoy, AWS API Gateway).