Two Technicians Cannot Both Reserve the Last Kit
A successful read does not reserve the stock it showed. Put the expected version and remaining-stock rule inside the actual write.
13 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
Testing the Precondition at the Write Boundary
BOTIE
This is the same concurrency problem encountered in the SQL versioned update. The syntax changes, while the reasoning stays useful: the application states which existing state would make its proposed action valid, and the database checks that condition atomically within the operation’s supported scope.
Sources for this history
Both Screens Say a Kit Is Available
Two technicians open the same stock record. Each sees enough parts and submits a reservation. If each program writes a quantity calculated from its earlier screen, one reservation can overwrite the other.
The decision needs to be checked where the change happens. Botie will combine the record version with the stock rule, then apply the update only if that complete condition still holds.
Invented maintenance records. These deterministic local models expose a storage decision; they do not run a distributed database or establish its guarantees.
Step 1 of 1
The request carries the assumptions behind its action
This invented request reserves three kits.
| Field | Current record | Request assumption |
|---|---|---|
| Identity | seal-kit | seal-kit |
| Version | 4 | expects 4 |
| Units | 8 | needs 3 |
Identity
- Current record
- seal-kit
- Request assumption
- seal-kit
Version
- Current record
- 4
- Request assumption
- expects 4
Units
- Current record
- 8
- Request assumption
- needs 3
An earlier screen is a proposal, not a reservation
The stale-state condition and stock invariant independently affect acceptance.
Reserve against the record that was actually read
Check identity and revision on one branch, sufficient stock on another, then connect their joint decision to the update.
BOTIE
Accepted flag and stock record
{
"accepted": true,
"record": {
"id": "seal-kit",
"version": 5,
"units": 5
}
}Follow the operations
1. Current stock
{ "id": "seal-kit", "version": 4, "units": 8 }2. Reservation proposal
{ "id": "seal-kit", "expected_version": 4, "amount": 3 }3. Check the reservation's expected version
true
4. Check the requested stock amount
true
5. Require both conditions
true
6. Publish the guarded reservation
{ "accepted": true, "record": { "id": "seal-kit", "version": 5, "units": 5 } }7. Result
{ "accepted": true, "record": { "id": "seal-kit", "version": 5, "units": 5 } }
This is a deterministic transition model, not a concurrent database, lock or idempotency service.
1. Separate a precondition from the proposed effect
The precondition asks whether this is the intended stock record, whether its version still matches, and whether enough units remain. The effect decreases units and increments the version. These are separate pieces even when one database command evaluates them together.
Our local function returns an accepted flag and a record. On rejection it keeps the entire old record. That makes the outcome inspectable without pretending a rejected reservation succeeded.
In the real service, the check and effect must share the database’s atomic write boundary.
2. A version identifies the state the caller used
The request carries expected_version from the read that informed the technician’s decision. A successful change advances the record’s version. An old request then fails even if its proposed quantity would be legal in isolation.
Every writer participating in this protocol must advance the version consistently. A maintenance script that updates units without doing so can invalidate the assumption. Blindly replacing expected_version with the latest one turns a conflict into an unreviewed new decision.
A version check is useful only when all relevant writes follow its contract.
Check yourself
Questions on Parts 1 to 2
Question 1 of 2
Should the complete condition pass?
Use the information below.
Current units are 5 at version 5. A request needs 3 units but expects version 4.3. Build the guarded change with ordinary records
Python dictionaries let us inspect the components independently. Check the identifier and version, then compare requested units with available units. Combine the booleans before changing a copied dictionary.
The browser runs one function invocation at a time. It demonstrates the state transition and rejection behaviour, but the function itself is not an atomic database write and is not safe as a concurrent inventory service.
The challenge asks for the transition model so we can test what an accepted or rejected command should mean.
Keep a stale proposal from changing a record
The current stock has moved to revision five while the proposal still expects four.
current = {"id":"seal-kit", "version":5, "units":5}
proposal = {"id":"seal-kit", "expected_version":4, "amount":3}
version_matches = current["version"] == proposal["expected_version"]
enough_stock = current["units"] >= proposal["amount"]
print(version_matches, enough_stock)
print(version_matches and enough_stock)False True
FalseThere are enough kits, but that fact does not repair the proposal’s stale assumption.
4. Send a fixed update expression and bound values
The server should construct the permitted operation. The caller supplies validated values, not an arbitrary update expression or a dictionary of database operators. Tenant scope comes from authenticated application context and must be checked before issuing the storage command.
Use a service role limited to the necessary table and operations. Attribute-name placeholders and value bindings keep expression structure separate from values; they do not replace authorisation. A partition key containing north cannot prove the requester belongs to north.
The following reference performs a real write only in an explicitly prepared external development table.
Run in your own environment
A DynamoDB reservation guarded at the service
The table has the string partition key id and an existing seal-kit item with numeric version and units.
External setup: install boto3, create a disposable development table and seed the named item.
Use an authorised development role through the SDK credential chain. Do not place AWS credentials in the browser exercise or source file.
import boto3
from botocore.exceptions import ClientError
table = boto3.resource("dynamodb", region_name="us-east-1").Table("maintenance_stock_dev")
try:
result = table.update_item(
Key={"id": "seal-kit"},
UpdateExpression="SET #u = #u - :amount, #v = #v + :one",
ConditionExpression="#v = :expected AND #u >= :amount",
ExpressionAttributeNames={"#u":"units", "#v":"version"},
ExpressionAttributeValues={":amount":3, ":one":1, ":expected":4},
ReturnValues="ALL_NEW",
)
print(result["Attributes"])
except ClientError as error:
if error.response["Error"]["Code"] == "ConditionalCheckFailedException":
print("Re-read and reconcile the reservation")
else:
raiseOnly an explicit conditional rejection takes the reconciliation path. Transport failures require separate outcome handling.
Official implementation documentation. Documentation checked .
5. A missing response is different from a rejected condition
If the server applies the update but the response is lost, the client cannot conclude that nothing happened. Repeating an unconditional decrement can reserve twice. Repeating the original versioned request will fail after a successful first application, but that failure alone cannot tell the client whether its own first attempt or another writer changed the record.
A production reservation needs a stable operation identity and a documented receipt or idempotency mechanism, with its scope and retention understood. Reconcile an unknown result before presenting another reservation as a new action.
The exercise has no transport and no persistent receipts. Its accepted flag describes only this local invocation.
6. Name the boundary of the invariant
A single-item condition can protect that item’s stock. It does not automatically reserve a kit in one record and book an engineer in another as one unit. Multi-item invariants require a supported transaction mechanism or an explicit workflow with compensation and visible partial states.
Test a repeated request, insufficient stock and an incorrect identity. Then test the actual server operation with concurrent callers before relying on it in production.
The next lesson considers operations whose replicas can merge safely after disconnected work.
Check yourself
Questions on Parts 1 to 6
Question 1 of 2
What is the honest outcome before reconciliation?
Use the information below.
A reservation request times out after being sent.Try it with Botie
Build with Botie
Write and run Python