Merchant API is a Java 21 / Spring Boot 3.5 backend for merchant onboarding and idempotent payment transaction processing. The project demonstrates production-minded REST design, database migrations, stateless authentication, request throttling, keyset pagination, structured errors, and containerized local infrastructure.
- Java 21
- Spring Boot 3.5
- Spring Web, Validation, Security, Actuator
- Spring Data JPA / Hibernate
- Oracle JDBC with Flyway SQL migrations
- Redis-backed request rate limiting
- JWT bearer authentication and merchant API keys
- RFC 7807
ProblemDetailerrors - Structured JSON logging with correlation IDs
- Springdoc OpenAPI / Swagger UI
- Docker Compose for Oracle Free, Redis, and the API
- Testcontainers integration tests
- k6 benchmark script
POST /api/v1/merchants
GET /api/v1/merchants/{merchantId}
POST /api/v1/transactions
GET /api/v1/transactions?afterId={cursor}&limit={size}
GET /api/v1/merchants/{merchantId}/transactions?afterId={cursor}&limit={size}
Transaction creation requires:
X-API-Key: <merchant-api-key>
Idempotency-Key: <unique-request-key>
Administrative endpoints require:
Authorization: Bearer <admin-jwt>
Swagger UI is available at:
http://localhost:8080/swagger-ui.html
Runtime configuration is supplied through environment variables. Copy .env.example to .env for local Docker Compose runs and replace every placeholder with development-only values.
Required variables:
ORACLE_PASSWORD
SPRING_DATASOURCE_USERNAME
SPRING_DATASOURCE_PASSWORD
JWT_SECRET
REDIS_HOST
REDIS_PORT
RATE_LIMIT_CAPACITY
RATE_LIMIT_REFILL_PER_MINUTE
The default Spring profile intentionally does not provide database or JWT secret fallbacks. Missing runtime secrets fail application startup instead of silently using unsafe credentials.
Flyway migrations are split by database profile:
src/main/resources/db/migration/oracle
src/main/resources/db/migration/h2
The API stores API keys as SHA-256 hashes in api_key; raw API keys are never persisted. Idempotency records are pruned by a scheduled cleanup job after the configured retention window.
Transaction creation uses database-backed idempotency plus a pessimistic merchant-row lock. This keeps duplicate requests from creating multiple transaction rows and gives future merchant balance/state transitions a clear isolation point.
Ledger reads use keyset pagination through afterId and limit instead of offset pagination, keeping large-table scans predictable.
The isolation rationale is documented in:
docs/adr/0002-concurrency-and-isolation.md
Health probes:
GET /health/live
GET /health/ready
Logs are emitted as JSON and include correlation_id when callers pass:
X-Correlation-ID: <request-correlation-id>
If no correlation id is supplied, the API generates one and returns it in the response header.
.\mvnw.cmd clean compile
.\mvnw.cmd clean test
.\mvnw.cmd clean verifyclean test runs unit, WebMvc, security, and rate-limit tests against H2. clean verify also runs Failsafe integration tests with Testcontainers-backed Oracle XE and Redis when Docker is available. Local runs skip the container tests if Docker is unreachable; CI runners provide Docker, so the real Oracle/Redis integration path is exercised on push and pull request builds.
Create .env from .env.example, then start the stack:
docker compose up --buildThe API is exposed on localhost:8080, Oracle on localhost:1521, and Redis on localhost:6379.
Install k6, then run:
k6 run benchmarks/transaction-create.k6.js `
-e BASE_URL=http://localhost:8080 `
-e MERCHANT_ID=1 `
-e MERCHANT_API_KEY=<merchant-api-key> `
-e RPS=25 `
-e DURATION=1mThe script reports p50, p95, p99 latency and request failure rate. Tune RPS, VUS, MAX_VUS, and DURATION to explore throughput limits.
GitHub Actions runs on every push and pull request to main:
.github/workflows/ci.yml
The workflow uses Java 21, Maven dependency caching, Docker-enabled Ubuntu runners, a pre-pulled Oracle XE test image, and ./mvnw --batch-mode clean verify.