Clear expired reservations out of carts overnight
Java · Spring Boot · intermediate · greenfield
Carts hold stock for 30 minutes per line (`reservedUntil`), and nothing ever clears the stale lines, so abandoned carts keep showing items that are long gone from the reservation system. This adds a nightly job that walks all carts, drops the lines whose reservation has lapsed, and saves the carts it changed. The pass logs how many lines it removed.
Shop backend on Spring Boot 3.3 (Java 21) with Spring Data JPA. Existing, not shown: `Cart` (`getId()`, and `List<CartItem> getItems()`, a `@OneToMany(orphanRemoval = true)` list, so lines removed from it are deleted when the cart is saved; Hibernate's `PersistentBag` wraps an `ArrayList` and iterates with that list's fail-fast iterator), `CartItem` (an `@Entity` with `Long id`, `sku`, `quantity` and `Instant reservedUntil`, read through the accessors `id()`, `sku()`, `quantity()` and `reservedUntil()`, equal by id; ids are unique), `CartRepository` (`List<Cart> findAllWithItems()` loads every cart with its lines in one query; `Cart save(Cart)`), a `Clock` bean, and `@EnableScheduling` on the application class. Only the classes in the diff are new.
Requirements
- Every night at 03:30 UTC, remove from every cart each line whose reservation has run out. A line is expired when its `reservedUntil` (never null) is not after the current instant, so a line that expires exactly now counts as expired. Lines with a later `reservedUntil` are left alone.
- Whatever lines a cart holds, after a successful run no cart contains an expired line. A cart that had no expired line is not saved.
- The run returns the number of lines removed across all carts, and the job logs it.
- Volume is small: a few thousand carts with at most 50 lines each, loaded in one query and cleaned in one transaction. If the run throws, the transaction rolls back and the next night's run starts again from scratch.
Files touched
- src/main/java/com/acme/shop/cart/CartCleanupService.java
- src/main/java/com/acme/shop/cart/CartCleanupJob.java
--- src/main/java/com/acme/shop/cart/CartCleanupService.java
+package com.acme.shop.cart;
+
+import java.time.Clock;
+import java.time.Instant;
+
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class CartCleanupService {
+
+ private final CartRepository carts;
+ private final Clock clock;
+
+ public CartCleanupService(CartRepository carts, Clock clock) {
+ this.carts = carts;
+ this.clock = clock;
+ }
+
+ /**
+ * Removes every cart line whose reservation has run out and returns how
+ * many lines were removed. A line is expired once {@code reservedUntil}
+ * is not after the current instant.
+ */
+ @Transactional
+ public int removeExpiredItems() {
+ Instant now = clock.instant();
+ int removed = 0;
+ for (Cart cart : carts.findAllWithItems()) {