Skip to content
Aabha AI Academy

Module 4 of 9 · Lesson 10 of 26

Protect capacity decisions in one transaction

Work through protect capacity decisions in one transaction using a runnable reference, a focused regression check and a local extension.

Read in any order. All lessons stay open, including after an unanswered or incorrect check.

In this lesson you will protect capacity decisions in one transaction. Work with the Equipment Rental API in the downloadable lab. The reference is a complete solution with separate lesson checks, so you can inspect the answer, make a deliberate local change and verify its behavior.
Two requests can both observe spare capacity and then overbook it. The safe decision must read current reservations and write the new rental within one serialized transaction. reserve first locks the equipment row with SELECT FOR UPDATE, computes active quantity, rejects an oversized request, and inserts the rental before commit. A second reservation for the same equipment waits, then sees the first committed rental.

The parent row is the coordination point. Every path that changes active capacity must use the same protocol, including check-in in the final feature. Locking only existing rental rows cannot protect the case where no rental exists yet. A Python lock in one worker is also insufficient when multiple processes handle requests. Use a consistent lock order to reduce deadlocks, and expect real systems to need timeout and retry policies around contention. This example uses PostgreSQL's default read-committed behavior and locks equipment before rental state. A capacity conflict is a deliberate 409 outcome, not an infrastructure failure. Returning success before the transaction commits would defeat the guarantee.
Worked source: equipment/services.py, reserve.

The lesson10 check starts two worker threads at a barrier. Each requests all two units of the same item. Exactly one must reserve and the other must receive capacity_unavailable. The database sum must stay at two. This checks the race using independent PostgreSQL sessions, not sequential mocked responses.
pythonCopyable
def reserve(session, equipment_id, owner_id, quantity):
    if quantity < 1:
        raise DomainError("invalid_quantity", "Request at least one unit.", 422)
    with session.begin():
        item = session.scalar(
            select(Equipment).where(Equipment.id == equipment_id).with_for_update()
        )
        if item is None:
            raise missing()
        used = session.scalar(
            select(func.coalesce(func.sum(Rental.quantity), 0)).where(
                Rental.equipment_id == equipment_id, Rental.status == "active"
            )
        )
        if used + quantity > item.quantity:
            raise DomainError(
                "capacity_unavailable", "Not enough equipment units are available."
            )
        rental = Rental(equipment_id=equipment_id, owner_id=owner_id, quantity=quantity)
        session.add(rental)
        session.flush()
        result = RentalOut.model_validate(rental)
    return result
TerminalPython 3.13 virtual environment; Docker running; extracted lab directory
python run_checks.py -k lesson10

Expected result The selected lesson test passes against a new temporary PostgreSQL database; the container is removed afterward.

Keep for reference

Equipment Rental lab and lesson checks

ZIP containing Python source, real Alembic migrations, 26 lesson checks, a dependency lock and text instructions. Extract it before following the local exercise.

Download Equipment Rental lab and lesson checks

Practise locally

Temporarily remove with_for_update in a copy and add a barrier between reading capacity and inserting. Demonstrate the overbooking defect, then restore the lock and require one winner. Do not leave the barrier in production code. Add a test with different equipment items to show that unrelated capacity decisions do not need the same parent lock.

The lesson check verifies the reference behavior. Add your own assertions for your change. Local practice is not uploaded or scored by this learning release.

Pause and reflect

What failure does this lesson prevent, and which assertion in lesson10 would expose it?

Use a concrete input, expected result and limitation from your local work. Saving a reflection does not certify the project.

Optional knowledge check

What makes the capacity decision safe across API workers?

All capacity-changing paths coordinate on the same database row inside a transaction.

Correct. The shared database lock serializes the read-and-write decision across processes.

Each worker has its own Python lock and checks stock before starting a transaction.

Try another answer. Separate process locks and stale pre-transaction reads do not coordinate the shared database.

Lock only the existing active rental rows.

Try another answer. The empty-rental case has no row to lock; coordinate on the equipment parent.

This practice does not assess your project or award a certificate.

Your reading progress

Progress is saved in this browser when storage is available.

Sign in to save across devices · Create an optional account