Skip to content

The uniqueness guard

The uniqueness guard is the decision model’s simplest case: the decision is “has this happened before?”, and the answer must hold at the instant of the write. It is what enforces “a course is opened at most once”, “an email registers at most one account”, “an order is placed once”.

An append condition is a query plus an after position. Leave after at its default, Position::ZERO, and the condition reads as “fail if any event matches at all”, across the whole log, not just since some point. That is the guard: the append succeeds only if nothing matching the query already exists.

let course_tag = format!("course:{course}");
let event = Event::new("CourseOpened", &[course_tag.as_str()], b"{}".to_vec())?;
// No `after`: the guard means "fail if a CourseOpened for this course already exists".
let guard = AppendCondition::new(Query::item(QueryItem::new(
vec![EventType::new("CourseOpened")?],
Tags::new([Tag::new(&course_tag)?])?,
)));
match client.append([event], Some(guard)) {
Ok(_) => Ok(true),
// A conflict means the course already exists (or is being opened concurrently).
Err(ClientError::Server {
code: ErrorCode::Conflict,
..
}) => Ok(false),
Err(err) => Err(Box::new(err)),
}

The query is precise: CourseOpened events carrying this course’s tag. It does not guard against opening a different course, and it does not guard against unrelated events on the same course, so two different courses opening concurrently never contend.

You could read the log, see no CourseOpened for this course, and then append. Between the read and the append, another writer could open the same course, and you would both succeed. The guard removes the gap: the check and the write are one atomic operation on the writer, so exactly one of two concurrent opens wins and the other is told it conflicts.

That is why the uniqueness guard omits after rather than reading and comparing. There is no position to read up to, because the guarantee is not “since I looked” but “ever”.

A conflict here means the course already exists. That is a durable conflict, not an advisory one: retrying will hit the same existing event and fail again. The caller treats it as a definite “no”, which is why the example returns false rather than looping. The difference between an advisory and a terminal conflict is the subject of Handling conflicts.