Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
target/
.env
.ideal
Comment thread
Tparfaite marked this conversation as resolved.
*.iml
.vscode/
.DS_Store
41 changes: 41 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.igirepay</groupId>
<artifactId>idempotency-gateway</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

<!-- This makes it a Spring Boot project -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
Comment thread
Tparfaite marked this conversation as resolved.
<relativePath/>
</parent>

<properties>
<java.version>17</java.version>
</properties>

<dependencies>
<!-- Gives us REST API capability (like Flask in Python) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
Comment thread
Tparfaite marked this conversation as resolved.

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
12 changes: 12 additions & 0 deletions src/main/java/com/igirepay/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.igirepay;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}
88 changes: 88 additions & 0 deletions src/main/java/com/igirepay/controller/PaymentController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.igirepay.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;

@RestController
@RequestMapping("/api")
public class PaymentController {
// Stores: idempotency_key -> saved response
private final ConcurrentHashMap<String, ResponseEntity<Map<String, Object>>> responseCache = new ConcurrentHashMap<>();

// Stores: idempotency_key -> original request body (for conflict detection)
private final ConcurrentHashMap<String, Map<String, Object>> requestCache = new ConcurrentHashMap<>();

// Stores: idempotency_key -> lock (for race condition handling)
private final ConcurrentHashMap<String, ReentrantLock> lockMap = new ConcurrentHashMap<>();
Comment thread
Tparfaite marked this conversation as resolved.

@PostMapping("/process-payment")
public ResponseEntity<Map<String, Object>> processPayment(
@RequestHeader(value = "Idempotency-key", required = false) String idempotencyKey,
@RequestBody Map<String, Object> requestBody) throws InterruptedException {

// GUARD: Missing Idempotency-Key header
if (idempotencyKey == null || idempotencyKey.isBlank()) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(Map.of("Error", "Missing Idempotency-key header"));
}

// Get or create a lock for this key
lockMap.putIfAbsent(idempotencyKey, new ReentrantLock());
ReentrantLock lock = lockMap.get(idempotencyKey);

// Acquire the lock — blocks duplicate requests until we're done
lock.lock();

try {
// Case 1: Key already exists
if(responseCache.containsKey(idempotencyKey)){
//Check if request body is different
Map<String, Object> originalBody = requestCache.get(idempotencyKey);
if(!originalBody.equals(requestBody)) {
return ResponseEntity
.status(HttpStatus.CONFLICT)
.body(Map.of("error", "Idempotency key already used for different request body"));
}

//same body return cached response
ResponseEntity<Map<String,Object>> cached = responseCache.get(idempotencyKey);
return ResponseEntity
.status(cached.getStatusCode())
.header("X-Cache-Hit", "true")
.body(cached.getBody());
}

// Case 2: New request , process it
// save the request body first
requestCache.put(idempotencyKey, requestBody);

Thread.sleep(2000);

// EXtract amount and currency from request body
Object amount = requestBody.get("amount");
Object currency = requestBody.get("currency");
Comment thread
Tparfaite marked this conversation as resolved.

// Build the response
Map<String, Object> responseBody = Map.of(
"status", "success",
"message", String.format("Charged %s %s", amount, currency),
"idempotencyKey", idempotencyKey
);

ResponseEntity<Map<String, Object>> response = ResponseEntity
.status(HttpStatus.CREATED)
.body(responseBody);
responseCache.put(idempotencyKey, response);

return response;
} finally {
lock.unlock();
}
}
}
1 change: 1 addition & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
server.port=8080