A production-grade payment clearing and settlement backend built with Java 21 / Spring Boot 3.2, implementing core acquiring-bank workflows: authorization, capture, reversal, fraud screening, rate limiting, settlement, and event streaming.
┌──────────────────────────────────────────────────────────────┐
│ REST API (port 8080) │
│ Spring Boot 3.2 · Tomcat 10.1 │
└────────────────────────┬─────────────────────────────────────┘
│
┌───────────────┼────────────────┐
▼ ▼ ▼
Authorization Fraud Engine Rate Limiter
Service (Chain of (Redis Token
(@Version Responsibility) Bucket)
optimistic lock)
│ │ │
└───────────────┼────────────────┘
▼
┌──────────────────┐
│ PostgreSQL 15 │ ←── Flyway migrations (V1, V2)
│ (JPA/Hibernate) │
└─────────┬────────┘
│
┌─────────┴────────┐
│ Apache Kafka │ ←── transaction-events (6 partitions)
│ (event stream) │
└─────────┬────────┘
│
┌─────────┴────────┐
│ Spring Batch │ ←── Hourly settlement job
│ (settlement) │ Double-entry ledger
└──────────────────┘
| Layer | Technology |
|---|---|
| Language | Java 21 |
| Framework | Spring Boot 3.2.0 |
| Web | Spring MVC · Tomcat 10.1 |
| Persistence | Spring Data JPA · Hibernate 6.3 · PostgreSQL 15 |
| Cache / Rate Limit | Spring Data Redis · Token Bucket |
| Messaging | Apache Kafka 3.6 · 6 partitions |
| Batch / Settlement | Spring Batch · hourly cron |
| Schema Migrations | Flyway 9.22 (V1, V2) |
| Testing | JUnit 5 · Mockito · Testcontainers |
| Coverage | JaCoCo 0.8.11 |
| Build | Maven 3 · Spring Boot Maven Plugin 3.2 |
- ISO 8583-aligned response codes (00 Approved, 05 Do Not Honor, 51 Insufficient Funds, 54 Expired, 62 Restricted)
- Optimistic locking via
@Version— prevents double-spend under concurrent requests without serializing writes - Idempotency via Redis — duplicate requests within TTL window return the cached response
- Merchant and card validation at authorization time
- Partial capture support (capture less than the authorized amount)
- Pre-capture void (cancel before settlement)
- Post-capture refund (reverse after settlement)
- Explicit state machine:
AUTHORIZED → CAPTURED → SETTLED / REFUNDED
- Chain-of-responsibility pattern — rules are pluggable, independently testable, zero coupling
- Velocity check — blocks cards exceeding N transactions per time window
- Amount threshold — flags single transactions above configurable limit
- Geo mismatch — detects card-present transactions in unexpected regions
- First failing rule short-circuits the chain
- Redis token bucket per merchant — correct across multiple application instances
- Returns
X-RateLimit-RemainingandX-RateLimit-Resetheaders on every response - HTTP 429 with
Retry-Afteron exhaustion
- Hourly scheduled job aggregates all
CAPTUREDtransactions - Double-entry ledger: debit merchant, credit acquirer
- Sum-zero integrity enforced via assertion tests on every settlement run
- Full job history persisted by Spring Batch in the database
- Every payment state change publishes an event to
transaction-events(6 partitions) - Consumer group
payment-consumer-grouppersists events toevent_logtable for audit trail
src/
├── main/java/com/payments/
│ ├── PaymentProcessingApplication.java
│ ├── api/
│ │ ├── controller/ # TransactionController, SettlementController
│ │ ├── request/ # AuthorizationRequest, CaptureRequest, ReversalRequest
│ │ ├── response/ # TransactionResponse, SettlementResponse, ApiResponse
│ │ └── GlobalExceptionHandler.java
│ ├── batch/
│ │ ├── SettlementBatchConfig.java
│ │ └── SettlementJobScheduler.java
│ ├── config/ # KafkaConfig, RedisConfig, JacksonConfig, SecurityConfig
│ ├── domain/
│ │ ├── entity/ # Transaction, Card, Merchant, LedgerEntry, SettlementRecord, EventLog
│ │ └── enums/ # TransactionStatus, ResponseCode, LedgerEntryType
│ ├── kafka/ # TransactionEventPublisher, TransactionEventConsumer, TransactionEvent
│ ├── repository/ # 6 Spring Data JPA repositories
│ └── service/
│ ├── fraud/ # FraudRuleEngine, VelocityCheckRule, AmountThresholdRule, GeoMismatchRule
│ ├── AuthorizationService.java
│ ├── CaptureService.java
│ ├── ReversalService.java
│ ├── LedgerService.java
│ ├── RateLimiterService.java
│ └── SettlementService.java
├── main/resources/
│ ├── application.yml
│ └── db/migration/
│ ├── V1__initial_schema.sql
│ └── V2__seed_data.sql
└── test/java/com/payments/
├── concurrency/ # ConcurrencyTest — 20-thread overdraw
├── fraud/ # FraudRuleTest — 10 rule scenarios
├── integration/ # TransactionLifecycleIntegrationTest (Testcontainers)
└── service/ # AuthorizationServiceTest, CaptureServiceTest,
# ReversalServiceTest, RateLimiterServiceTest,
# LedgerIntegrityTest, CardTest
- Java 21+
- Maven 3.8+
- Docker Desktop
docker-compose up postgres redis zookeeper kafka -dmvn spring-boot:runApplication starts on http://localhost:8080. Flyway applies migrations automatically on first boot.
# Linux / macOS
curl http://localhost:8080/actuator/health
# PowerShell
Invoke-WebRequest -UseBasicParsing http://localhost:8080/actuator/health | Select-Object -ExpandProperty ContentExpected: {"status":"UP"}
POST /api/v1/payments/authorize
Content-Type: application/json
{
"amount": 100.00,
"currency": "USD",
"cardNumber": "4111111111111111",
"merchantId": "MERCH001",
"idempotencyKey": "unique-key-001"
}POST /api/v1/payments/{transactionId}/capture
Content-Type: application/json
{
"amount": 100.00
}POST /api/v1/payments/{transactionId}/reverseGET /actuator/health
GET /actuator/metrics# All tests (requires Docker for Testcontainers)
mvn test
# View coverage report (after mvn test)
# macOS
open target/site/jacoco/index.html
# Windows
start target/site/jacoco/index.htmlKey properties in application.yml:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/payments
data:
redis:
host: localhost
port: 6379
kafka:
bootstrap-servers: localhost:9092
consumer:
group-id: payment-consumer-group
auto-offset-reset: earliest
payments:
fraud:
velocity-limit-per-minute: 5
single-txn-amount-threshold: 100000
rate-limit:
requests-per-minute: 100
burst-size: 20
settlement:
cron: "0 0 * * * *"
batch-size: 100| Checkpoint | Output |
|---|---|
| Spring Boot started | Started PaymentProcessingApplication in 12.067s |
| PostgreSQL connected | HikariPool-1 - Start completed |
| Flyway migrations applied | Successfully applied 2 migrations to schema "public" |
| JPA repositories wired | Found 6 JPA repository interfaces |
| Tomcat listening | Tomcat started on port 8080 (http) |
| Kafka consumer joined | partitions assigned: [transaction-events-0..5] |
| Actuator health | HTTP 200 {"status":"UP"} |
Why optimistic locking? Pessimistic locking serializes all auth requests through a single row lock, killing throughput under load. @Version-based optimistic locking allows concurrent reads and only retries on the rare actual conflict — correct behaviour with near-zero overhead at normal concurrency levels.
Why chain-of-responsibility for fraud? Rules need to be added, removed, and reordered without touching existing code. CoR makes each rule a standalone, independently testable unit with no coupling between rules.
Why Redis token bucket over in-memory rate limiting? In-memory state breaks across multiple application instances. Redis gives a single shared counter that enforces limits correctly whether the system runs one pod or ten.
Why Spring Batch for settlement? Settlement is inherently chunk-oriented — read N captured transactions, process, write ledger entries. Spring Batch provides restart/retry semantics, configurable skip policies, and persistent job history out of the box.