Skip to content

Preconditions

This guide explains how to use preconditions in EventSourcingDB to ensure that events are only written when certain expectations are met. Preconditions allow you to express business or consistency constraints declaratively – directly as part of a write request.

Why Preconditions Matter

In many systems, it's not enough to simply write an event. You often want to do so only if certain conditions hold – for example, that no prior events exist for a subject, or that no changes have occurred since your last read.

Preconditions let you enforce such guarantees without additional roundtrips or manual checks. They support use cases like optimistic locking, subject initialization, and conflict detection in concurrent environments.

How Preconditions Work

Preconditions are evaluated by EventSourcingDB as part of the same transaction that writes your events. If all preconditions are met, the events are written. If any of them fail, nothing is written, and the operation is aborted with HTTP status code 409 Conflict. The message names the precondition that does not hold by its place in the request and its type, such as state conflict: /preconditions/2: precondition isSubjectPopulated does not hold. If several do not hold, it names the first of them in the order of the request, except that isEventQlQueryTrue preconditions are evaluated after all others, since their queries may read every event.

A precondition that can not be evaluated, such as an isEventQlQueryTrue precondition whose query does not parse, fails while it is evaluated, or does not return exactly one boolean, is rejected with 400 Bad Request and a message that names the reason.

You can include preconditions by adding a preconditions array to the request body, alongside the events array.

Available Precondition Types

EventSourcingDB supports the following preconditions:

isSubjectPristine

This precondition checks that a given subject has no events yet. It is typically used when introducing a subject for the first time – for example, when acquiring a book or registering a new user. It ensures that the subject is empty and that no prior write has occurred.

{
  "type": "isSubjectPristine",
  "payload": {
    "subject": "/books/42"
  }
}

Use this precondition when your event is meant to initialize a stream and should not be duplicated.

isSubjectPopulated

This precondition checks that a given subject has at least one event. It is the opposite of isSubjectPristine and is typically used when updating an existing subject – for example, when borrowing a book or updating a user profile. It ensures that the subject exists and has been initialized.

{
  "type": "isSubjectPopulated",
  "payload": {
    "subject": "/books/42"
  }
}

Use this precondition when your event is meant to modify an existing stream and should not operate on uninitialized subjects.

isSubjectOnEventId

This precondition checks that the most recent event for a subject has a specific ID. It is used to implement optimistic concurrency: ensuring that no new events have been written to the subject since you last read it.

{
  "type": "isSubjectOnEventId",
  "payload": {
    "subject": "/books/42",
    "eventId": "23"
  }
}

This is useful when writing a follow-up event that assumes the subject is in a known state. If another event has been added in the meantime, the write will be rejected – preventing lost updates or inconsistent state.

Checking a Subject and Everything Below It

On the v2 API, isSubjectPristine, isSubjectPopulated, and isSubjectOnEventId take an optional options object in their payload. With "recursive": true, a precondition applies to the subject together with all the subjects below it: isSubjectPristine holds if none of them has an event, isSubjectPopulated if any of them has one, and isSubjectOnEventId if the latest event among all of them has the given ID. The check reads only the latest event of the subtree from an index, never the events themselves, so it stays cheap however many events lie below the subject.

{
  "type": "isSubjectOnEventId",
  "payload": {
    "subject": "/orders/42",
    "eventId": "1234",
    "options": {
      "recursive": true
    }
  }
}

This makes a consistency boundary possible that spans several subjects, such as an order at /orders/42 with its items at /orders/42/items/...: the write is rejected if anything below the order has changed since event 1234.

isEventQlQueryTrue

This precondition evaluates an EventQL query and expects the result to be true. It allows you to express arbitrary consistency checks based on the current state of the event store.

{
  "type": "isEventQlQueryTrue",
  "payload": {
    "query": "FROM e IN events WHERE e.data.title == '2001 – A Space Odyssey' PROJECT INTO COUNT() == 0"
  }
}

Use this precondition when other types are insufficient – for example, to enforce uniqueness, detect conflicts, or implement custom business rules declaratively.

While an EventQL precondition is evaluated, every other write waits, so that nothing can come in between; reads go on meanwhile. Its query reads every event of the store unless its WHERE clause bounds the event ID, as e.id > 1000 does, or asks for the events of exactly one subject, as e.subject == '/books/42' does, or of exactly one event type, as e.type == 'io.eventsourcingdb.library.book-acquired' does, or bounds the time of the events, as e.time > '2026-10-20T00:00:00Z' AS DATETIME does, with the other conditions joined by AND, however selective the rest of the clause is, so a precondition like the one above makes writing slower the more events the database holds. To check a single subject, or a subject together with everything below it, prefer isSubjectPristine, isSubjectPopulated, or isSubjectOnEventId. Since such writes are evaluated one after the other, writes that carry such a precondition more often than their query takes make every write wait longer and longer: keep them rare, or bound the query by event ID or by time where the rule you check allows it.

A rule allows such a bound when it only concerns what happened after a point you know, such as the last event your decision read. This precondition, for example, checks that the reader /readers/23 has borrowed no book since the event with ID 1000:

{
  "type": "isEventQlQueryTrue",
  "payload": {
    "query": "FROM e IN events WHERE e.id > 1000 AND e.type == 'io.eventsourcingdb.library.book-borrowed' AND e.data.borrowedBy == '/readers/23' PROJECT INTO COUNT() == 0"
  }
}

Transactional Behavior

All preconditions are evaluated as part of the same transaction as the events. This guarantees atomicity:

  • If all preconditions pass, all events are written.
  • If any precondition fails, no events are written.

This makes preconditions a safe and expressive way to guard against race conditions and enforce domain invariants.

Best Practices

  • Use isSubjectPristine when initializing a subject to prevent accidental re-creation.
  • Use isSubjectPopulated when updating a subject to ensure it has been initialized before applying changes.
  • Use isSubjectOnEventId when updating a subject based on prior reads to ensure consistency.
  • Don't solely rely on preconditions for business logic – use them to enforce technical guarantees, not to replace domain modeling.
  • Keep in mind that preconditions are optional, but highly recommended in collaborative or distributed environments.

Designing with Preconditions

Preconditions help you build safer, more predictable event-sourced systems. They allow the database to participate in consistency checks – reducing the need for custom locking logic and race-condition handling in your application.

Used thoughtfully, they provide a clean and reliable foundation for modeling workflows, enforcing invariants, and coordinating concurrent operations.