PayFlow is a robust and resilient payment processing application built with Spring Boot. It demonstrates several important architectural patterns for building reliable distributed systems, including idempotency, the outbox pattern for reliable event publishing, and secure webhook handling.
- Idempotent Payment Creation: Prevents duplicate payment creation for the same request.
- Asynchronous Event Publishing: Uses the outbox pattern with Kafka to reliably publish payment events.
- Secure Webhook Handling: Verifies webhook signatures using HMAC-SHA256 and protects against replay attacks.
- Database Migrations: Uses Flyway for managing database schema changes.
- Resilient by Design: Incorporates Resilience4j for fault tolerance patterns.
- Java 17
- Spring Boot 3
- Maven
- H2 Database (for local development)
- Flyway for database migrations.
- Apache Kafka for messaging.
- Docker for containerization.
- Lombok to reduce boilerplate code.
- Resilience4j for fault tolerance.
- Docker and Docker Compose
- Java 17 or later (for running outside of Docker)
- Maven 3.6 or later (for running outside of Docker)
The easiest way to run PayFlow and its dependencies (like Kafka) is by using Docker Compose.
-
Build the application JAR:
./mvnw clean package
-
Start the application with Docker Compose:
docker-compose up --build
The application will be available at http://localhost:8080.
If you prefer to run the application without Docker, you'll need to have Kafka running separately.
-
Start Kafka: Ensure you have a Kafka broker running and accessible at
localhost:9092. -
Run the Spring Boot application:
./mvnw spring-boot:run
The application will use an in-memory H2 database by default.
The application is configured in src/main/resources/application.properties. When running with Docker Compose, the environment variables in docker-compose.yml will override these settings.
Default application.properties for local development:
spring.application.name=payflow
spring.datasource.url=jdbc:h2:mem:payflow
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.show-sql=true
flyway.enabled=true
flyway.baseline-on-migrate=true
kafka.bootstrap-servers=localhost:9092
resilience4j.retry.instances.downstream.maxAttempts=3
resilience4j.retry.instances.downstream.waitDuration=1s
resilience4j.circuitbreaker.instances.downstream.failureRateThreshold=50
resilience4j.circuitbreaker.instances.downstream.slidingWindowSize=10
resilience4j.circuitbreaker.instances.downstream.waitDurationInOpenState=10s- URL:
/payments - Method:
POST - Headers:
Idempotency-Key: A unique key for the request (e.g., a UUID).
- Request Body:
{ "amount": 1000, "currency": "USD" } - Success Response:
{ "paymentId": "...", "status": "PENDING" }
- URL:
/payments/{id}/status - Method:
PUT - Path Variables:
id: The ID of the payment to update.
- Query Parameters:
status: The new status (e.g., "COMPLETED", "FAILED").
- Success Response:
{ "paymentId": "...", "status": "COMPLETED" }
- URL:
/webhooks/psp - Method:
POST - Headers:
X-Signature: The HMAC-SHA256 signature of the request body.X-Timestamp: The timestamp of the request.
- Request Body:
{ "event_id": "...", "payment_id": "...", "status": "COMPLETED", "ts": 1678886400 }
The POST /payments endpoint is idempotent. If the same request is sent multiple times with the same Idempotency-Key header, the payment will only be created once, and subsequent requests will receive the same response.
When a payment is created or its status is updated, an event is written to the events_outbox table in the same transaction. A background process (OutboxPublisher) periodically polls this table and publishes the events to a Kafka topic (payments.events). This ensures that events are published reliably, even if the application crashes after the database transaction is committed but before the event is sent to Kafka.
The PaymentEventConsumer consumes events from the payments.events Kafka topic. It uses the events_inbox table to ensure that each event is processed exactly once, preventing duplicate processing in case of message redelivery from Kafka.
The /webhooks/psp endpoint is secured using the following mechanisms:
- HMAC Signature Verification: The
X-Signatureheader is verified to ensure that the webhook was sent by the trusted PSP and that the payload has not been tampered with. - Timestamp Verification: The
X-Timestampheader is checked to prevent replay attacks. Webhooks that are too old are rejected. - Replay Guard: The
webhook_replay_guardtable is used to store the IDs of processed webhook events, preventing the same event from being processed multiple times.