Book packed orders with the carrier and mark them shipped

Java · Spring Boot · advanced · modification

Adds `ShipmentWorker`, a scheduled job that picks up packed orders, books each one with the carrier and stamps it shipped with the tracking code and time. The carrier call stays outside any database transaction so a slow booking never holds a connection or a row lock, and a `CarrierException` is logged and leaves that order packed for the next run while the rest of the batch carries on. `OrderRepository` gets the query for packed orders, oldest first.

Fulfilment service on Spring Boot 3.3 (Java 21), Spring Data JPA with Hibernate 6.5 on PostgreSQL 16 at the default READ COMMITTED isolation; scheduling is enabled with the default single-threaded scheduler, and a `Clock` bean exists. Existing and unchanged: `Order`, a JPA entity mapped to `orders` with `id`, `reference`, `customerId`, `shippingAddress`, `status` (`OrderStatus`: `PLACED`, `PACKED`, `SHIPPED`, `CANCELLED`), `packedAt` (set on every packed order), `trackingCode` and `shippedAt`; it has no `@Version` column, no entity listeners and no `@DynamicUpdate`. `Order.markShipped(trackingCode, shippedAt)` sets the status to `SHIPPED` and the two fields, and throws `IllegalStateException` unless the order is `PACKED`. The address endpoint (existing) calls `OrderRepository.updateAddress` and answers 200 when it updated one row, otherwise 404 or 409. `CarrierClient` (existing) sets connect and read timeouts and wraps every failure in `CarrierException`; both live in `com.acme.fulfilment.shipping`, and `CarrierException` is unchecked. No transaction is open around the scheduled method, and the service has no other scheduled jobs.

Requirements

Files touched

--- src/main/java/com/acme/fulfilment/orders/OrderRepository.java
 package com.acme.fulfilment.orders;
 
+import java.util.List;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Modifying;
             """)
     int updateAddress(@Param("id") long id, @Param("customerId") long customerId, @Param("address") String address);
+
+    List<Order> findByStatusOrderByPackedAtAsc(OrderStatus status);
 }
 
--- src/main/java/com/acme/fulfilment/shipping/ShipmentWorker.java
+package com.acme.fulfilment.shipping;
+
+import com.acme.fulfilment.orders.Order;
+import com.acme.fulfilment.orders.OrderRepository;
+import com.acme.fulfilment.orders.OrderStatus;
+import java.time.Clock;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ShipmentWorker {
+
+    private static final Logger log = LoggerFactory.getLogger(ShipmentWorker.class);
+
+    private final OrderRepository orders;

Review this PR

Java practice