Retries are everywhere.
HTTP clients retry on timeouts. Load balancers retry. Message queues retry. Humans double-click buttons. And when retries hit endpoints that cause real-world side effects, things break in subtle ways.
Every time I hit this, I ended up rebuilding the same pattern: some variation of idempotency keys, unique constraints, or ad-hoc state tables. Eventually I decided to turn that pattern into a small API: ExactOnce.
The core idea is simple: represent a real-world side effect as an "action," and enforce that it can transition to "consumed" exactly once.
Most systems try to solve this with idempotency keys:
That works well for request deduplication. It does not model lifecycle. It doesn't answer: is this action expired? Has it been canceled? Was it consumed with the wrong PIN three times? What was the final state?
I wanted something more explicit — an object that moves through well-defined states.
This doesn't replace idempotency keys for all use cases. It's a different model — one where the side effect itself is modeled as a state machine. If that's overkill for your case, you probably don't need this.
Idempotency makes repeats return the same result, but it does not guarantee a single state transition under concurrency. ExactOnce enforces one atomic change from active -> consumed, and all later calls return the same stable outcome.
Internally, each action is stored in DynamoDB with fields like:
actionId — unique identifieruserId — owning useractiveAtMs / expiresAtMs — validity windowconsumedAt — set atomically on consumptionconsumedReason — caller-supplied contextpinHash — optional PIN protectionpayload — encrypted arbitrary dataThe action starts in a pending or active state. It can transition to: consumed or expired. Only one transition to consumed is allowed. Cancellation is a separate API operation, not a state in the public model.
The guarantee hinges on a single DynamoDB UpdateItem call with a ConditionExpression that must all be true simultaneously:
consumedAt must not existcanceledAt must not existactiveAtMs <= nowexpiresAtMs > nowconst params = { TableName: 'Actions', Key: { actionId }, UpdateExpression: 'SET consumedAt = :now, consumedReason = :reason', ConditionExpression: ` attribute_exists(actionId) AND attribute_not_exists(consumedAt) AND attribute_not_exists(canceledAt) AND activeAtMs <= :nowMs AND expiresAtMs > :nowMs AND (attribute_not_exists(pinHash) OR pinHash = :pinHash) `, ExpressionAttributeValues: { ':now': new Date().toISOString(), ':nowMs': Date.now(), ':reason': reason, ':pinHash': pinHash ?? null, }, }; try { await dynamodb.update(params).promise(); // ✓ Consumed. Exactly once. } catch (err) { if (err.code === 'ConditionalCheckFailedException') { // Determine which condition failed and return appropriate error: // → already_used, expired, not_active, invalid_pin, etc. throw mapConditionalFailure(actionId); } throw err; }
If any of those conditions fail, DynamoDB throws ConditionalCheckFailedException. That exception is not an error condition — it is the concurrency control mechanism.
Multiple clients can attempt to consume the same action simultaneously. Only one succeeds. The rest deterministically fail. No distributed locks. No transactions across services. Just one atomic conditional write.
Because the state lives in the same record, every outcome is stored and explicit:
409 already_used410 expired// Consumed atomically — one winner 200 { "state": "consumed", "consumedAt": "2026-02-22T..." } // Already consumed — deterministic 409 409 { "error": "already_used", "consumedAt": "2026-02-22T..." } // TTL elapsed — gone by design 410 { "error": "expired" } // Never existed 404 { "error": "action_not_found" }
DynamoDB's conditional updates make this clean. Single-digit millisecond writes. Strong consistency on demand. Atomic compare-and-set semantics. Built-in TTL for cleanup. The table becomes the state machine.
There's no separate lock table, no Redis SETNX expiry race, no saga orchestration. The action record is the lock. Consuming it is the lock release. They're the same operation.
Because I kept rebuilding it. Exactly-once semantics show up in magic login links, coupon redemption, ticket entry, webhook deduplication, license activation, approval flows. The pattern is the same every time.
ExactOnce is intentionally narrow. It's not a workflow engine. It's not an auth system. It's just a primitive for "this must happen once." If you've built this internally before, I'm looking for developers who've hit this problem and want to stop rebuilding it.
ExactOnce is in private beta. Free to use. Just a working API and your feedback.
Request Access →