Start learning free

LESSON 76 OF 173 · BACKEND: APIS WITH A DATABASE

All or Nothing in Java

Practice

An order changes several rows: the order itself, one row per item, and the stock of every product it buys. If an item fails halfway, the shop must not keep the first half. An order has to be all or nothing.

Without help, JdbcTemplate saves each statement the moment it runs. The database gives you all or nothing with a transaction, and in Spring you get one with @Transactional: every change the method makes waits in it, and Spring saves them all at once when the method ends. Calling setRollbackOnly() on the running transaction tells Spring to throw them all away instead. Here is the idea on a money transfer between two accounts:

@Transactional
@PostMapping("/transfer")
Object transfer(@RequestBody Map<String, Object> data) {
    db.update("UPDATE accounts SET balance = balance - ? WHERE id = ?", data.get("amount"), data.get("from"));
    db.update("UPDATE accounts SET balance = balance + ? WHERE id = ?", data.get("amount"), data.get("to"));
    Map<String, Object> sender = db.queryForMap("SELECT balance FROM accounts WHERE id = ?", data.get("from"));
    if ((int) sender.get("balance") < 0) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        return ResponseEntity.status(409).body(Map.of("error", "Not enough money"));
    }
    return Map.of("ok", true);
}

Both updates are saved, or neither: money never leaves one account without reaching the other. 409 Conflict is the status for this: the request is fine in itself, but it clashes with the state of the data. Spring rolls back by itself only when an exception leaves the method; a normal return, even with a 409, commits. So call setRollbackOnly() before the return, since nothing after a return runs.

Run in this order on a fresh shop, your POST /orders must give these answers:

POST /orders  {"customer_id": 5, "items": [{"product_id": 3, "quantity": 1}, {"product_id": 1, "quantity": 2}]}
    →  201  {"id": 8}
GET /products/3  →  200  {"id": 3, "name": "Lamp", "price": 29, "category": "home", "stock": 0}
POST /orders  {"customer_id": 4, "items": [{"product_id": 4, "quantity": 10}, {"product_id": 3, "quantity": 1}]}
    →  409  {"error": "Not enough stock", "product_id": 3}
GET /orders/9/items  →  404  {"error": "Order not found"}
GET /products/4  →  200  {"id": 4, "name": "Pen", "price": 2, "category": "office", "stock": 120}
POST /orders  {"customer_id": 2, "items": [{"product_id": 7, "quantity": 1}, {"product_id": 99, "quantity": 1}]}
    →  404  {"error": "Product not found", "product_id": 99}
POST /orders  {"customer_id": 4, "items": [{"product_id": 4, "quantity": 10}]}
    →  201  {"id": 9}

The second order fails on the lamp, so its pens are not saved either: there is no order 9, the pens' stock is still 120, and the next good order takes id 9.

Your task: in POST /orders, take each item's quantity off its product's stock. Answer 409 when there isn't enough stock and 404 when the product doesn't exist. Make placeOrder one transaction with @Transactional and mark it rollback-only before both answers, so a failed order leaves nothing behind.


Assignment
Follow the Coding Tutorial to update the stock, check it before each item, and roll back every order that can't be completed.


Hint
Both checks go right below the lines that read products and product, before anything is written for that item. The empty check comes first: products.get(0) throws on an empty list. The imports for @Transactional and TransactionAspectSupport are given.


STEP 1 OF 9
    Ctrl + Enter to check

    Db Transaction in Java

    Keep going with Java1,600+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free