# Project: an inventory reservation and fulfillment service

Canonical page: https://yishto.com/en/learn/project-labs/lab-inventory-reservations/

Author: [Yishto](https://yishto.com/en/about/)

Connect inventory reservation, confirmation, expiry, and compensation through transactions, conditional transitions, and an outbox, handling repeated messages.

Published: 2026-11-10
Updated: 2026-11-10

## Define conditional inventory transitions

The first requirement is that two valid reservations cannot occupy the same available unit. This project uses synthetic items to practice reservation, confirmation, expiry, and compensation without real payments. Begin with a modular application and one database. Split services only when ownership, load, or independent releases justify the new boundaries.

Define `available` as stock that can still be reserved. A `pending` reservation occupies it; confirmation consumes it; expiry or cancellation releases it. Physical receipts, returns, and losses need their own records. For this fixed-stock exercise, available, pending, and consumed quantities should sum to the initial amount, and available stock must stay nonnegative.

Write the precondition for every operation. Confirmation accepts only an unexpired `pending` reservation. Expiry releases only `pending` stock, and cancellation cannot release an already expired record. State names do not prevent races: checking the old state and applying the transition must be one constrained database operation.

## Design request identity with transactions

A `stock` record holds the item and available quantity. A `reservation` holds a request key, item, quantity, state, expiry, and version. Define the uniqueness scope, such as tenant plus request key. Reusing a key must compare the payload; a changed item or quantity cannot reuse an earlier success.

Reserve within a transaction by checking or locking the stock row, deducting only when enough remains, and inserting the reservation. A conditional quantity update with an affected-row check is another option. Separate check and decrement requests race. Merely opening a transaction does not make arbitrary statements and isolation levels prevent overselling. Verify locking order, deadlock retries, and rollback with the chosen database.

A response timeout means the client did not obtain the result; the reservation may have committed. Query by the logical request key before deciding how to recover, and reuse that key on retries. Disabling a button helps usability, while database uniqueness and server-side idempotency determine whether repeated requests change stock.

## Check repeated release with an offline model

The example starts with two synthetic units. The dictionary records reservation state, and expiry releases stock only from `pending`. Repeated submission and repeated expiry return failure without changing quantity again.

```python
stock = 2
reservations = {}
def reserve(key):
    global stock
    if key in reservations or stock == 0:
        return False
    stock -= 1
    reservations[key] = "pending"
    return True
def expire(key):
    global stock
    if reservations.get(key) != "pending":
        return False
    reservations[key] = "expired"
    stock += 1
    return True
assert reserve("R-1") and not reserve("R-1")
assert expire("R-1") and not expire("R-1")
assert reservations["R-1"] == "expired" and stock == 2
print(stock, reservations)
```

The output is `2 {'R-1': 'expired'}`. This verifies one sequential path: one deduction, one release, and no extra effect from duplicate calls. It includes no locks, concurrent transactions, persistence, or real expiry clock, so it cannot establish concurrency safety.

It also omits confirmation and multi-unit validation. Add a confirmation branch that rejects expired records. At the endpoint, require a positive integer quantity and reject zero, negative values, and invalid types. The full implementation must compare repeated-request content rather than treating every repeated key as the same failure.

## Serialize confirmation against expiry

An expiry scanner and a confirmation request can both observe `pending`. Both must perform a conditional database transition and include the inventory effect in the same transaction. A scanner must not add stock based on a stale observation. After confirmation wins, the expiry update should affect zero rows and finish according to the current state.

Use server-defined expiry and a documented time boundary. This exercise can reject confirmation at or after expiry, checking time and state inside the confirmation transaction. A late payment-success message then needs explicit compensation or human resolution; it must not silently revive an expired reservation. A successful time check does not make external payment and inventory one atomic transaction.

Record transition reasons, request identity, and versions to investigate timeouts and unexpected deductions. Keep real payment credentials out of the exercise. Check invariants periodically and trace differences to records and events. Clamping a negative count to zero would conceal the defect.

## Use an outbox for committed message intent

When confirmation must notify fulfillment, write the business changes and an outbox record in the same database transaction. A publisher reads committed outbox entries and sends them later. This preserves the notification intent if the application crashes after the database commit but before sending.

The publisher can still crash after successful delivery and before marking an entry sent, causing a repeat. RabbitMQ publisher confirms concern the publisher-to-broker interaction; they do not establish completed consumer business work. Consumer acknowledgements are separate. A consumer can deduplicate through a unique event record committed with its business update, then acknowledge after that commit.

Compensation across databases or external fulfillment is a new business action, not a rollback of all history. Give it an identity, retryable states, and a human reconciliation path. Do not assume exactly-once network delivery yields exactly-once business effects. Route permanent failures into an observable handling path rather than an endless requeue loop.

Authorize ownership when reading a committed reservation; an unguessable request key is insufficient. Define whether insufficient-stock outcomes are stored idempotently. If a failure is frozen, replenishment does not change that key's result, and a new intent needs a new key. Keep this retry policy consistent in endpoint documentation and tests.

## Expand a complete slice into a fault matrix

Implement stock reads, reservation, and request-key lookup first, followed by confirmation, expiry, and audit. Add outbox publishing after the database invariants are established. Distinguish insufficient stock, invalid input, conflicting key reuse, and temporarily unknown results in responses; each needs a different recovery action.

Test two clients competing for the last unit, confirmation racing expiry, a mid-transaction failure, a lost response after commit, failed marking after publication, and a lost acknowledgement after consumption. Repeatedly deliver the same event and check final state and quantity. Database integration tests and fault injection must establish real concurrency behavior beyond offline assertions.

If services are later separated, mark which records each transaction covers, where messages repeat, and when querying or compensation is necessary. Additional services should preserve the same inventory meaning and invariants.

## Sources & further reading

- [PostgreSQL transactions](https://www.postgresql.org/docs/current/tutorial-transactions.html)
- [Publisher confirms and consumer acknowledgements](https://github.com/rabbitmq/rabbitmq-website/blob/main/docs/confirms.md)
- [HTTP semantics — RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html)

Sources checked on 2026-11-10. Examples are teaching data, not production measurements.

## Related guides

- [Use database constraints to protect business correctness](https://yishto.com/en/learn/service-data/database-indexes-and-transactions/)
- [Keep payments and product entitlements consistent](https://yishto.com/en/learn/product-building/payments-entitlements-and-webhooks/)
