Start learning free

LESSON 162 OF 173 · BACKEND: BACKEND WITH AI

The AI Oversells in Go

Practice

An AI assistant wrote the shop's buy handler: a customer sends a quantity, and the product's stock goes down. The AI did it in two steps. A SELECT reads the stock and Go checks it, then an UPDATE takes the quantity off:

app.HandleFunc("POST /products/{id}/buy", func(w http.ResponseWriter, r *http.Request) {
    id, _ := strconv.Atoi(r.PathValue("id"))
    var data map[string]int
    json.NewDecoder(r.Body).Decode(&data)
    var stock int
    err := db.QueryRow("SELECT stock FROM products WHERE id = ?", id).Scan(&stock)
    if err == sql.ErrNoRows {
        writeJSON(w, 404, map[string]string{"error": "Product not found"})
        return
    }
    if stock < data["quantity"] {
        writeJSON(w, 409, map[string]string{"error": "Not enough stock"})
        return
    }
    db.Exec("UPDATE products SET stock = stock - ? WHERE id = ?", data["quantity"], id)
    writeJSON(w, 200, map[string]int{"id": id, "bought": data["quantity"]})
})

With one customer at a time, it works. But a web server handles requests side by side. When two customers buy the last lamp at the same moment, both SELECTs can read 1 before either UPDATE runs. Both checks pass, both customers get the lamp, and the stock ends at -1.

The fix: don't read first. Put the check inside the write, in the UPDATE's WHERE. The database runs one write at a time, so the row changes only if the condition is still true at that very moment: one atomic step. Then res.RowsAffected() tells you how many rows changed. Here is the idea for a coupon that can be used a few times:

query := "UPDATE coupons SET uses_left = uses_left - 1 WHERE code = ? AND uses_left > 0"
res, _ := db.Exec(query, "SPRING")
changed, _ := res.RowsAffected()
fmt.Println(changed)   // 1: the coupon was used, 0: none left

Your rewrite must give these answers. The third request plays the race: another customer buys the last lamp just before your UPDATE runs.

POST /products/1/buy   {"quantity": 3}  →  200  {"bought": 3, "id": 1}
GET  /products/1                        →  200  {"category": "kitchen", "id": 1, "name": "Mug", "price": 12, "stock": 7}
POST /products/3/buy   {"quantity": 1}  →  409  {"error": "Not enough stock"}
GET  /products/3                        →  200  {"category": "home", "id": 3, "name": "Lamp", "price": 29, "stock": 0}
POST /products/5/buy   {"quantity": 7}  →  409  {"error": "Not enough stock"}
POST /products/5/buy   {"quantity": 6}  →  200  {"bought": 6, "id": 5}
POST /products/99/buy  {"quantity": 1}  →  404  {"error": "Product not found"}

Your task: rewrite the POST /products/{id}/buy handler so the stock check and the change are one UPDATE. Answer 409 when it changed no row, and 404 when the product doesn't exist.


Assignment
Follow the Coding Tutorial to rewrite the buy handler with one atomic UPDATE, and use res.RowsAffected() to answer 409 or 404 when it changed nothing.


Hint
Add the stock check to the UPDATE's WHERE, and pass one value per ?, in order. db.Exec saves the change right away: there is no separate commit. Look the product up only after the UPDATE changed nothing: that lookup explains the failure, it never decides who gets the stock.


STEP 1 OF 12
    Ctrl + Enter to check

    Ai Oversell in Go

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