Screen orders with the fraud-scoring service at checkout

Java · Spring Boot · intermediate · modification

Checkout now asks the fraud-scoring service for a risk score before charging the card. `FraudScreening` posts the order id and amount through the shared `RestClient.Builder`, declines scores of 80 or more, and approves anything lower. If the service is unreachable or answers with an error or garbage, the order is approved and flagged for manual review so the team can look at it later, and a warning is logged. `CheckoutService` calls it between loading the order and charging the payment.

Shop backend on Spring Boot 3.3 (Java 21), Tomcat with the default 200 request threads, `spring-boot-starter-web` only (no Apache HttpClient or other HTTP client on the classpath). `RestClient.Builder` is the one auto-configured by Spring Boot. `fraud.base-url` and `fraud.api-key` are set in every environment. `Order` has `getId()` and `getTotalCents()`; `OrderRepository` has `findById`, `flagForReview(long id)` and `markPaid(long id, String paymentRef)`; `CheckoutResult` has `PAID`, `DECLINED` and `PAYMENT_FAILED`; `PaymentGateway.charge` throws the unchecked `PaymentException` when a payment is refused. The scoring service is a third party; the service is in production use and answers in well under a second when healthy. `CheckoutService.checkout` runs on the request thread of the checkout endpoint (existing).

Requirements

Files touched

--- src/main/java/com/acme/shop/checkout/CheckoutService.java
     private final OrderRepository orders;
     private final PaymentGateway payments;
+    private final FraudScreening fraud;
 
-    public CheckoutService(OrderRepository orders, PaymentGateway payments) {
+    public CheckoutService(OrderRepository orders, PaymentGateway payments, FraudScreening fraud) {
         this.orders = orders;
         this.payments = payments;
+        this.fraud = fraud;
     }
 
     public CheckoutResult checkout(long orderId) {
         Order order = orders.findById(orderId).orElseThrow();
+        FraudDecision decision = fraud.screen(order.getId(), order.getTotalCents());
+        if (!decision.approved()) {
+            return CheckoutResult.DECLINED;
+        }
+        if (decision.manualReview()) {
+            orders.flagForReview(order.getId());
+        }
         try {
             String ref = payments.charge(order.getId(), order.getTotalCents());
--- src/main/java/com/acme/shop/checkout/FraudDecision.java
+package com.acme.shop.checkout;
+
+public record FraudDecision(boolean approved, boolean manualReview) {
+
+    public static FraudDecision approve() {
+        return new FraudDecision(true, false);

Review this PR

Java practice