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.sqlcreatespaymentsandidempotency_requests. - Default datasource is configurable via
SPRING_DATASOURCE_*(seeapplication.yml). Docker Compose includes apostgresservice for local development.
B. Idempotency
- Optional
Idempotency-Keyheader 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-keys→ROLE_MERCHANT.gateway.security.admin-api-keys→ROLE_ADMIN+ROLE_MERCHANT.- Method security:
@PreAuthorize("hasAnyRole('MERCHANT','ADMIN')")on payment endpoints. gateway.security.enabled=false(used by thetestprofile) disables API key enforcement and@EnableMethodSecurityfor slice/unit tests.
D. Rate limiting
- Bucket4j greedy refill bucket per API key (defaults: 120 requests / minute, configurable).
gateway.ratelimit.enabled=falsein 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.urldefaults tohttp://localhost:8080for the simulator; theprodprofile default ishttps://localhost:8080as 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,prometheusexposed (tune for your environment). - Micrometer Prometheus registry on the classpath.
logback-spring.xml: JSON console logging on theprodprofile via Logstash encoder.
G. Resilience
- Resilience4j
@Retryand@CircuitBreakeronBankClient.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
HashMapwith 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_dateMM/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 thetestprofile (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).