Prevent duplicate operations through idempotency
Recover open actions and begin additional intention only through an explicit new operation.

The same intention is not automatically a new operation
Someone presses Submit twice or receives no clear outcome after an interrupted response. The application may then receive several requests for one intended action. Idempotency here means treating repetition of that action without creating another business effect. The server must recognise the operation. The number of HTTP arrivals alone does not identify how many reservations the person actually intended to request.
Our original fictional example is a workshop initially offering three places. One request can hold one or two. A complete PHP core commits the reservation with a unique operation receipt. Its JavaScript interface retains identifier and selected values after an unclear response, creating another intention only when the person explicitly begins one after an authoritative outcome. It does not infer a new request merely from another network attempt.
No real booking or customer data is involved. Eighty-three executed conditions connect actual PHP processes with browser journeys. Unlike automated webhook reception, this example centres on human intention, visible recovery and the deliberate start of another action. Those meanings are designed into the interface instead of being left to accidental transport behaviour or a different random identifier generated after every failure.
Identify the action before transmitting it
The browser creates a random UUID when the person first meaningfully submits. It remains the same for subsequent attempts at that operation. The selected quantity is also copied into a simple saved payload. Retry must not accidentally combine the old identifier with newly edited values. The example therefore keeps selection controls disabled while the operation remains open and its outcome is not yet clear.
The identifier is neither a password nor permission. It supports correlation inside an already established application context. Generating a fresh identifier on every network retry would defeat that purpose: the server could not distinguish repeated intention from an expressly requested additional reservation. Key lifecycle therefore belongs to business interaction design rather than being treated as a low-level networking convenience disconnected from the person’s task.
The crypto.randomUUID method requires an appropriate browser context. Execution here uses a trusted local development address. A real website needs review of supported browsers and secure context. The demonstration does not substitute a weak handmade random number, nor does a well-generated UUID establish authentication or access control. Randomness and correlation cannot answer who is permitted to perform the underlying action.
Combine browser convenience with authoritative server handling
The interface disables its trigger during an outstanding request. Under the controlled delayed-request condition, a double click therefore generates one transmission. That is useful but incomplete. Another client, several windows or a later retry can still create more requests. Authoritative handling also belongs in shared database state, which survives beyond one button, tab or JavaScript callback and can coordinate competing callers.
The double-click test deliberately holds the browser request briefly before continuing to the real server. This is a controlled condition, not measured Internet latency. Independent checks also start two PHP processes simultaneously with the same operation identifier. Both receive the same receipt while producing one reservation. The combination examines client suppression and server repeat handling separately instead of crediting a button state with guarantees it cannot supply.
Different mechanisms consequently have appropriately bounded roles. Disabling a trigger reduces unnecessary transmissions; a stable identifier recognises repetitions; the transaction limits their business effect. Executing them together establishes whether those layers agree for this particular workflow. No CSS attribute can make the server’s capacity decision or replace a persisted operation receipt once a request has left the browser.
Scope the identifier to the correct actor
The database key combines trusted actor and operation identifier. Two distinct authorised contexts can consequently use the same identifier without receiving each other’s receipt. An executed test uses two fictional actors and obtains separate reservations while shared capacity permits both. Capacity rules remain independent of identity scoping: separating receipts does not give each actor a separate imaginary supply of workshop places.
The application supplies actor context to the PHP function; a free JSON property cannot choose it. The isolated HTTP fixture uses one fixed demo actor and implements no login. Production must derive identity and permission from its authoritative system. Checking the spelling of an actor identifier does not establish that someone authenticated successfully or is allowed to operate on the requested workshop.
Browser state also needs to fit the actual signed-in context. A form saved under an earlier login must not silently become the next person’s new action. This example has one fixed context. Real account changes require deliberate storage scoping, logout and resumption rules. Server-side receipt scoping is useful but does not, by itself, implement those broader session transitions or protect arbitrary client storage.
Compare the business values defining the intention
An existing key must not silently be reused with different values. The module fingerprints a fixed workshop identifier and integer quantity. Asking for two places instead of one under the same operation produces conflict. The original reservation and receipt remain unchanged. The server does not casually reinterpret the changed payload as another order or overwrite an already confirmed result to make the latest request appear successful.
Comparison is deliberately limited to this small typed pair. JSON property order is irrelevant, but a numeric string, boolean or fractional value is rejected. This is not a general canonical representation for arbitrary JSON documents. The contract states exactly which values and types determine action identity, avoiding accidental conversion rules that might differ between a browser, framework and database.
A signed external interface comparing unchanged body bytes may need a different rule. For this form, the same quantity in the same workshop defines matching business input. Another application must explicitly select its comparison meaning too. Unnoticed coercion or an excessively broad equality rule can reject legitimate retries or conceal changed intention. Choosing that meaning is part of the product contract rather than a universal property of hashing.
Commit capacity decision and receipt together
The core begins an immediate SQLite transaction and first looks for an existing operation. If none exists, it reads workshop capacity and already reserved quantities. It inserts a reservation only when enough capacity remains, then stores the original HTTP outcome as the receipt. Both commit together and both roll back on failure, preventing an effect from surviving without the record needed to recognise its repetition.
Capacity checking happens within the same write boundary. Distinct new operations must not independently promise the same last places. In an executed case, two PHP processes each request two places from a workshop holding three. Exactly one receives a reservation; the other gets an authoritative unavailable outcome. Persisted quantity remains two. This tests competing intentions rather than only repeated copies of one operation.
That boundary concerns this writing path in one SQLite file. Other capacity changes or reservation writers must honour the authoritative rules too. An external ticket provider is not automatically inside the transaction. The article claims no real event allocation or universal once-only guarantee across remote services. Evidence concerns the fictional collection and the actual operations exercised against it, not every possible production architecture.
Read the complete PHP operation core
The following module includes tables, the fictional workshop and processing. A successful first reservation returns status 201 and a stored body. An identical retry returns exactly that original body and status, with a separate replay indicator. Repeating the original status is therefore not a claim that another reservation was created. It preserves the defined response to the same operation while making reuse observable.
Contradictory use of an old identifier produces conflict. Invalid values are rejected before effects occur. Tests also force a database failure while inserting the receipt. The reservation created earlier within that transaction does not remain afterwards. This establishes a concrete rollback outcome rather than merely asserting that a transaction call exists in the implementation or assuming a successful happy path proves joint persistence.
<?php
declare(strict_types=1);
function reservationDatabase(string $path): PDO {
$db=new PDO('sqlite:'.$path,null,null,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
$db->exec('PRAGMA foreign_keys=ON');$db->exec('PRAGMA busy_timeout=5000');
$db->exec('CREATE TABLE IF NOT EXISTS workshops(id TEXT PRIMARY KEY,capacity INTEGER NOT NULL CHECK(capacity>=0))');
$db->exec("INSERT OR IGNORE INTO workshops VALUES('workshop-demo',3)");
$db->exec('CREATE TABLE IF NOT EXISTS reservations(id TEXT PRIMARY KEY,actor TEXT NOT NULL,workshop TEXT NOT NULL REFERENCES workshops(id),quantity INTEGER NOT NULL CHECK(quantity BETWEEN 1 AND 2),operation TEXT NOT NULL,UNIQUE(actor,operation))');
$db->exec('CREATE TABLE IF NOT EXISTS operations(actor TEXT NOT NULL,operation TEXT NOT NULL,fingerprint TEXT NOT NULL,http_status INTEGER NOT NULL,response TEXT NOT NULL,created_at INTEGER NOT NULL,PRIMARY KEY(actor,operation))');
return $db;
}
function reservePlaces(PDO $db,string $actor,string $operation,array $input,int $now): array {
// actor is already authenticated/authorised application context, not a JSON field.
if(!preg_match('/^[a-z][a-z0-9_-]{1,40}$/D',$actor))throw new InvalidArgumentException('Ungültiger Anwendungskontext.');
if(!preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/D',$operation))
return ['http_status'=>400,'body'=>['error'=>'operation_id'],'replayed'=>false];
$keys=array_keys($input);sort($keys);
if($keys!==['quantity','workshop']||!is_string($input['workshop'])||!preg_match('/^[a-z][a-z0-9-]{1,40}$/D',$input['workshop'])
||!is_int($input['quantity'])||$input['quantity']<1||$input['quantity']>2)
return ['http_status'=>422,'body'=>['error'=>'request_contract'],'replayed'=>false];
// Canonical only for this small explicitly typed tuple, not arbitrary JSON.
$fingerprint=hash('sha256',json_encode(['workshop'=>$input['workshop'],'quantity'=>$input['quantity']],JSON_THROW_ON_ERROR));
$db->exec('BEGIN IMMEDIATE');
try {
$find=$db->prepare('SELECT fingerprint,http_status,response FROM operations WHERE actor=? AND operation=?');$find->execute([$actor,$operation]);$old=$find->fetch(PDO::FETCH_ASSOC);
if($old) {
$db->exec('COMMIT');
if(!hash_equals($old['fingerprint'],$fingerprint))return ['http_status'=>409,'body'=>['error'=>'operation_conflict'],'replayed'=>false];
return ['http_status'=>(int)$old['http_status'],'body'=>json_decode($old['response'],true,512,JSON_THROW_ON_ERROR),'replayed'=>true];
}
$query=$db->prepare('SELECT capacity FROM workshops WHERE id=?');$query->execute([$input['workshop']]);$capacity=$query->fetchColumn();
if($capacity===false){$db->exec('COMMIT');return ['http_status'=>404,'body'=>['error'=>'unknown_workshop'],'replayed'=>false];}
$used=$db->prepare('SELECT COALESCE(SUM(quantity),0) FROM reservations WHERE workshop=?');$used->execute([$input['workshop']]);
if((int)$used->fetchColumn()+$input['quantity']>(int)$capacity) {
$status=409;$body=['status'=>'unavailable'];
} else {
$id=bin2hex(random_bytes(16));
$db->prepare('INSERT INTO reservations VALUES(?,?,?,?,?)')->execute([$id,$actor,$input['workshop'],$input['quantity'],$operation]);
$status=201;$body=['status'=>'reserved','reservation_id'=>$id,'quantity'=>$input['quantity']];
}
$db->prepare('INSERT INTO operations VALUES(?,?,?,?,?,?)')->execute([$actor,$operation,$fingerprint,$status,json_encode($body,JSON_THROW_ON_ERROR),$now]);
$db->exec('COMMIT');
return ['http_status'=>$status,'body'=>$body,'replayed'=>false];
}catch(Throwable $error){$db->exec('ROLLBACK');throw $error;}
}
An unclear response leaves the operation open
The local HTTP fixture saves a reservation, then deliberately sends no usable JSON once. The browser cannot confirm success, displays an unclear result and offers Retry. Quantity remains locked, and starting another operation is not offered yet. Unknown outcome is not relabelled as definite failure, because the actual server-side effect may already exist even when the interface cannot interpret its acknowledgement.
Retry uses the same identifier and copied values. The database returns the existing receipt and creates no second reservation. Browser and PHP actually execute this recovery. No random Internet fault rate is measured: the unusable response is deliberately introduced at a known point after database commit. It establishes that specific recovery route without pretending every possible network and server failure has been represented.
Other services may return another temporary problem, but an unknown outcome should not automatically create fresh intention. Doing so could turn a cautious retry into an unwanted additional effect. A finished application may add status inspection or other clarification routes according to its task. This bounded example uses the repeat-safe invocation of the same operation as its explicit resolution path.
Reload preserves the open operation
The browser saves identifier and copied payload in sessionStorage before transmission. Tests actually reload the page after the deliberate response problem and find the open operation retained. Loading does not submit it automatically. Instead, the same explicit retry route remains available, and only activating it asks the server again for the authoritative stored result of that operation.
This storage belongs to its browser context, not an unlimited durable business archive. Other windows, closed sessions or disabled storage require suitable product decisions. The example checks whether storage can be prepared and does not send a supposedly resumable operation after preparation fails. It promises no recovery across arbitrary devices or browsers and no permanent storage of every operation the application might perform.
Saved fixture values contain no confidential customer information. Real software needs a deliberate choice about client-side state. Users can alter browser data, so the server still checks types, permissions and comparison values. sessionStorage helps resumption; it does not make an unchecked client state authoritative or provide a security boundary merely because the saved object has an application-specific name.
Follow the complete browser flow
The following HTML defines the small fictional reservation surface. Its trigger becomes active only after script preparation. The example consequently claims no complete JavaScript-free alternative. It is an explicitly bounded application surface illustrating state and intention. A real site can develop a suitable server-rendered baseline, but that route must preserve the same operation identity rather than introducing an incompatible second submission model.
CSS and JavaScript belong to the same executed example. The interface distinguishes an open operation from an authoritative outcome. It creates no fresh identifier in the background while the previous action is unresolved. Once reservation or stored unavailability is confirmed, an explicit new action appears. This prevents an ordinary error branch from silently changing the business meaning of the person’s next activation.
<main><h1>Plätze im Beispielworkshop vormerken</h1>
<p>Fiktiver Workshop mit anfangs drei Plätzen. Dieser Entwurf führt keine reale Buchung aus.</p>
<form id="reservation-form"><label for="quantity">Anzahl der Plätze</label>
<input id="quantity" name="quantity" type="number" min="1" max="2" step="1" required value="1">
<button id="submit" disabled>Vormerken</button></form>
<p id="status" role="status"></p><p id="receipt"></p>
<button id="new-intent" type="button" hidden>Weitere Vormerkung beginnen</button></main>
*{box-sizing:border-box}body{margin:0;background:#f8f2e8;color:#202428;font:18px/1.5 Arial,sans-serif}main{max-width:46rem;margin:2rem auto;padding:2rem;border-top:6px solid #cc0712}h1{font-size:2rem;line-height:1.2}form{display:grid;gap:1rem;max-width:28rem}input,button{font:inherit;min-height:48px;border:1px solid #777;border-radius:4px;padding:.7rem 1rem;max-width:100%}button{background:#c90713;color:white;border-color:#c90713}button:disabled{opacity:.65}#status{font-weight:bold}#receipt{overflow-wrap:anywhere}:focus-visible{outline:3px solid #cd0712;outline-offset:4px}@media(max-width:500px){main{margin:1rem 0;padding:1.2rem}h1{font-size:1.7rem}}@media(prefers-color-scheme:dark){body{background:#1e2225;color:#fbf3e7}:focus-visible{outline-color:#ff9a90}input{background:#fff;color:#202428}}
const form=document.querySelector('#reservation-form'),quantity=document.querySelector('#quantity');
const submit=document.querySelector('#submit'),newIntent=document.querySelector('#new-intent');
const status=document.querySelector('#status'),receipt=document.querySelector('#receipt');
const storageKey='demo-workshop-intent-v1';
let intent=null,busy=false;
try {
const stored=sessionStorage.getItem(storageKey);
if(stored) {
const saved=JSON.parse(stored);
if(typeof saved.operation!=='string'||!/^[-a-f0-9]{36}$/.test(saved.operation)
||saved.payload?.workshop!=='workshop-demo'||!Number.isInteger(saved.payload.quantity)
||saved.payload.quantity<1||saved.payload.quantity>2)throw Error('Invalid saved intent');
intent=saved;quantity.value=String(intent.payload.quantity);quantity.disabled=true;
status.textContent='Offener Vorgang. Erneut versuchen.';submit.textContent='Erneut versuchen';
}
sessionStorage.setItem(storageKey+'-probe','1');sessionStorage.removeItem(storageKey+'-probe');
submit.disabled=false;
}catch{status.textContent='Der Vorgang konnte nicht vorbereitet werden.';}
form.addEventListener('submit',async event=>{
event.preventDefault();if(busy||!form.reportValidity())return;
try {
if(!intent) {
intent={operation:crypto.randomUUID(),payload:{workshop:'workshop-demo',quantity:quantity.valueAsNumber}};
sessionStorage.setItem(storageKey,JSON.stringify(intent));
}
}catch{intent=null;status.textContent='Der Vorgang konnte nicht vorbereitet werden.';return;}
busy=true;submit.disabled=true;quantity.disabled=true;newIntent.hidden=true;
status.textContent='Vormerkung wird geprüft.';receipt.textContent='';
try {
const response=await fetch('reserve.php'+location.search,{method:'POST',headers:{'Content-Type':'application/json','Idempotency-Key':intent.operation},body:JSON.stringify(intent.payload)});
const result=await response.json();
if(response.status===201&&result.status==='reserved'&&result.quantity===intent.payload.quantity
&&/^[a-f0-9]{32}$/.test(result.reservation_id)) {
status.textContent='Vormerkung gespeichert.';receipt.textContent='Vorgang: '+result.reservation_id;
newIntent.textContent='Weitere Vormerkung beginnen';newIntent.hidden=false;
} else if(response.status===409&&result.status==='unavailable') {
status.textContent='Keine passenden Plätze frei. Es wurde nichts vorgemerkt.';
newIntent.textContent='Auswahl ändern';newIntent.hidden=false;
} else throw Error('No authoritative outcome');
}catch{status.textContent='Ergebnis unklar. Erneut versuchen.';}
finally{busy=false;submit.disabled=false;submit.textContent='Erneut versuchen';}
});
newIntent.addEventListener('click',()=>{
if(busy)return;
try{sessionStorage.removeItem(storageKey);}catch{return;}
intent=null;quantity.disabled=false;quantity.value='1';receipt.textContent='';status.textContent='';
submit.textContent='Vormerken';newIntent.hidden=true;quantity.focus();
});
Begin an additional reservation explicitly
After a definite outcome, the person can choose the action meaning Begin another reservation. Only that removes prior browser state and unlocks selection. The next submission creates a new identifier. The test then makes a second reservation for two places. Together with the initial one-place request, all three initial places are held, showing that genuinely distinct intention remains possible rather than deduplicating everything with similar text.
The next new request receives authoritative unavailability, and that decision is stored for its operation identifier too. A separate server case later increases capacity, but retrying the old refused operation does not retroactively create a reservation. It returns its original receipt. Only another deliberately new operation can use changed capacity. This keeps historical processing outcome distinct from the latest resource state.
That rule must suit the product. Some applications need a new availability check, time-limited holds or cancellation. This contract intentionally implements neither automatic expiry nor cancellation. Those extensions need explicit treatment of original processing receipts and later current state. A stored success should not casually become a promise that a subsequently expired or cancelled real-world hold is still available.

Review 83 executed conditions in server and browser
The evidence combines PHP 8.4.27 with desktop Edge. Server cases cover first effect, retry, changed payload under an old key, shared identifier across actor scopes and joint capacity. Concurrent PHP processes share one receipt for one operation; competing new operations do not oversubscribe places. A deliberate receipt-write failure rolls the reservation back as well, establishing the particular atomic boundary exercised here.
Browser journeys cover a controlled delayed double click, unusable postcommit response, actual reload, same-operation retry and explicit new intention. Eight real browser POST requests are included across light and dark appearances. Result pages remain contained at 320, 390 and 1440 pixels. The explanatory image shows genuine captures of unclear and subsequently confirmed states from this same application code.
Invalid quantities, additional properties, malformed operation identifiers and an unknown workshop are checked too and create no extra reservations. These 83 conditions are functional evidence for the bounded arrangement. There is no load measurement, real booking or comprehensive authentication acceptance. The controlled data and source files remain clearly associated with the demonstration rather than being presented as findings about an existing production reservation service.
Plan receipts, recovery and extensions together
An idempotency receipt belongs to the state of its effect. Partial restoration or premature receipt deletion can make an old retry lose its identity. Retention therefore needs to match possible retry periods and recovery. The experiment deletes no receipts automatically. A production application needs an explicit lifecycle rather than assuming receipt storage can be independently cleaned without changing the meaning of future repeated requests.
Additional effects require attention too. Confirmation mail or an external booking request is not protected merely by the local transaction. Such work benefits from its own durable handover. The receipt should describe the effect actually confirmed, not imply another provider’s delivery guarantee. It also does not replace permission checking before allowing the person to initiate that business action in the first place.
Idempotency does not mean merging similar text generally. Identical selections may represent two intentionally separate actions. The explicit contract therefore connects user intention, repeat identifier and server state. Complete sources and executed counterexamples show that relationship. In a real product, its boundaries must then be adapted to the actual business operation, account transitions, persistence model and operational recovery routes.
Intent and outcome at a glance
| Action | Outcome in this example |
|---|---|
| Same actor, key and selection | Return original receipt without another reservation |
| Same key with changed selection | Conflict, preserve original effect |
| Unusable response after persistence | Remain unresolved and retry the same operation |
| Reload the view | Retain open operation without automatic submission |
| Explicitly begin another reservation | Unlock selection and identify the next operation separately |
| Competing new operations under scarce capacity | Check shared capacity inside the transaction |
Repeat-safe form checklist
- Define real intention and when it begins.
- Retain the operation identifier across retries.
- Bind selection to the open action without silent changes.
- Scope keys to authoritative actor and operation context.
- Define business comparison values and types.
- Handle effect, capacity and receipt within the appropriate transaction.
- Do not infer absent effect from an unusable response.
- Check reload and explicit creation of another operation.
- Exercise conflicts, concurrency and receipt-write failure.
- Develop retention, authentication, expiry and external effects separately.
Sources and review scope
Primary sources reviewed on 9 November 2026. Original fictional workshop, 83 executed PHP/Edge conditions and eight browser POST requests. Controlled unusable response after commit, no real booking and no complete authentication implementation.








