Skip to content

Idempotency and deduplication

A command handler often has to apply a command exactly once even when the caller retries: a network timeout, a redelivered message, or a double click must not place two orders. The fail_if_exists clause on an append condition makes that a single atomic guarantee.

An append condition’s boundary check ignores everything at or before after and fails if anything matching the query landed since. That is exactly right for a decision boundary (“nothing new since I read”). An idempotency check is different: it must assert the command’s key exists nowhere in the log, from the very beginning, regardless of how far the boundary has advanced.

One after cannot be both. Set it to your boundary position N and the dedupe check misses a duplicate that landed before N. Set it to 0 and the boundary check fails on every event your decision legitimately read. The two assertions need two different after values at once.

fail_if_exists is that second assertion: a query evaluated against the whole log (an implicit after = 0), OR’d with the boundary check. The append fails if either fires.

let dedupe_tag = format!("cmd:{key}");
let event = Event::new("OrderPlaced", [dedupe_tag.as_str()], b"{}".to_vec())?;
// `fail_if_exists` asserts this command's dedupe key exists nowhere in the log, independent of
// any boundary cursor. Its conflict is a distinct `AlreadyExists`, not a boundary `Conflict`.
let condition =
AppendCondition::exists_only(Query::item(QueryItem::with_tags(Tags::new([Tag::new(
dedupe_tag,
)?])?)));
match client.append([event], Some(condition)) {
Ok(_) => Ok(true),
// `AlreadyExists` means the command was already applied: a successful no-op, not an error
// to retry (a retry would only be rejected again).
Err(ClientError::Server {
code: ErrorCode::AlreadyExists,
..
}) => Ok(false),
Err(err) => Err(Box::new(err)),
}

Tag each command with a unique key (cmd:order-1) and guard the append on it. The first apply commits; any retry finds the key already present and is rejected, even after the log has moved on.

A fail_if_exists rejection is reported as AlreadyExists, distinct from the Conflict a boundary check raises. The distinction matters because the two call for opposite responses:

  • Conflict (boundary): your decision read stale data. Re-read, rebuild, and retry.
  • AlreadyExists (existence): the command was already applied. Treat it as a successful no-op; retrying will only be rejected again.

The error code is what tells the two apart; retryable is the orthogonal axis it always is. A durable conflict is terminal (retryable: false); a same-batch race is retryable: true. The example above keys on code alone, which is correct for a single dedupe tag: a same-batch race there is a real duplicate the winner already committed, so treating it as already-applied is right. If your existence query carries several tags, also honor retryable, since a same-batch match can be conservative (see Handling conflicts).

Because it is a separate clause, fail_if_exists composes with an ordinary boundary in one atomic condition:

let condition = AppendCondition::new(boundary_query)
.after(watermark)
.fail_if_exists(dedupe_query);

That is the case a single after cannot express, and the reason the clause exists: a decision whose boundary has legitimately advanced can still assert its command has never been applied. The boundary is checked first, so a genuine boundary conflict is reported as Conflict; only when the boundary passes does a duplicate surface as AlreadyExists.