diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1209fb0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +target/ +.env +.ideal +*.iml +.vscode/ +.DS_Store \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f241570 --- /dev/null +++ b/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + com.igirepay + idempotency-gateway + 1.0.0 + jar + + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + + 17 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/src/main/java/com/igirepay/Application.java b/src/main/java/com/igirepay/Application.java new file mode 100644 index 0000000..9785fa6 --- /dev/null +++ b/src/main/java/com/igirepay/Application.java @@ -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); + } + +} diff --git a/src/main/java/com/igirepay/controller/PaymentController.java b/src/main/java/com/igirepay/controller/PaymentController.java new file mode 100644 index 0000000..5d09d67 --- /dev/null +++ b/src/main/java/com/igirepay/controller/PaymentController.java @@ -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>> responseCache = new ConcurrentHashMap<>(); + + // Stores: idempotency_key -> original request body (for conflict detection) + private final ConcurrentHashMap> requestCache = new ConcurrentHashMap<>(); + + // Stores: idempotency_key -> lock (for race condition handling) + private final ConcurrentHashMap lockMap = new ConcurrentHashMap<>(); + + @PostMapping("/process-payment") + public ResponseEntity> processPayment( + @RequestHeader(value = "Idempotency-key", required = false) String idempotencyKey, + @RequestBody Map 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 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> 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"); + + // Build the response + Map responseBody = Map.of( + "status", "success", + "message", String.format("Charged %s %s", amount, currency), + "idempotencyKey", idempotencyKey + ); + + ResponseEntity> response = ResponseEntity + .status(HttpStatus.CREATED) + .body(responseBody); + responseCache.put(idempotencyKey, response); + + return response; + } finally { + lock.unlock(); + } + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..a3ac65c --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=8080 \ No newline at end of file