Docs / EventSourcingDB / Fundamentals / Preconditions

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#

Changed in 1.3Preconditions 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 write is refused with the error precondition-violated. Like every error of the API, it comes with status 200, as an item of the response stream, followed by the end item:

{"type":"error","data":{"code":"https://docs.eventsourcingdb.io/errors/precondition-violated","description":"/preconditions/2: precondition isSubjectPopulated does not hold","details":{"precondition":2}}}
{"type":"end","data":{}}

Since a refused write has the status 200 as well, tell it by its error item, not by the status code. The error names the precondition that does not hold by its place in the request, counting from 0: its description gives the place and the type, and its details give the place as precondition. 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. If your client comes from the v1 API, see Migrating to the v2 API.

A precondition that cannot be evaluated is reported with an error of its own rather than as one that does not hold, and nothing is written either: query-too-long if the query of an isEventQlQueryTrue precondition is longer than 100 KiB, which is checked before any precondition is evaluated, query-malformed if it does not parse, query-too-deep if it nests more than 256 levels or holds a chain of more than 10,000 operands, query-unevaluable if it fails while it is evaluated, and query-not-boolean if it does not return exactly one boolean.

Changed in 1.3You can include preconditions by adding a preconditions array to the body of a write, POST /api/v2/data/write-events, 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.

isSubjectPopulatedAdded in 1.2#

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 ItAdded in 1.3#

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.

This makes a consistency boundary possible that spans several subjects, such as a book at /books/42 with its copies at /books/42/copies/1, /books/42/copies/2, and so on. The following write borrows the second copy, but only if nothing below the book has changed since event 1234, the latest event you read before deciding:

curl \
  -X POST \
  -H "Authorization: Bearer <API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "source": "https://library.eventsourcingdb.io",
        "subject": "/books/42/copies/2",
        "type": "io.eventsourcingdb.library.book-borrowed",
        "data": {
          "borrowedBy": "/readers/23",
          "borrowedUntil": "2026-11-22"
        }
      }
    ],
    "preconditions": [
      {
        "type": "isSubjectOnEventId",
        "payload": {
          "subject": "/books/42",
          "eventId": "1234",
          "options": {
            "recursive": true
          }
        }
      }
    ]
  }' \
  http://localhost:3000/api/v2/data/write-events

If anything has been written to the book or to one of its copies since event 1234, the write is refused with precondition-violated, and the copy is not borrowed.

isEventQlQueryTrueAdded in 1.1#

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.

Added in 1.3Its query may have at most 100 KiB, as much as a query that is run on its own, see Limits. While an EventQL precondition is evaluated, every other write waits, so that nothing can come in between; reads go on meanwhile. So that no write holds up the others for long, the EventQL preconditions of a write are evaluated for at most five seconds, all of them together, counted from when the write gets its turn: a write whose preconditions take longer is rejected, with nothing written, with a 400 and query cannot be evaluated: evaluation exceeds limit of 5 seconds for the EventQL preconditions of a write on the v1 API and with the error query-unevaluable on the v2 API, and the other writes go on. Its query reads every event of the store unless its WHERE clause bounds the event ID, as e.id AS INT > 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 field of the event on the left of the comparison and 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, and is rejected once its query takes longer than five seconds. 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, up to five seconds for each of them ahead of it: 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 AS INT > 1000 AND e.type == 'io.eventsourcingdb.library.book-borrowed' AND e.data.borrowedBy == '/readers/23' PROJECT INTO COUNT() == 0"
  }
}

A write runs to its end once its request has arrived, also if its client goes away meanwhile, so a client that goes away cannot tell whether its write was carried out. How long such a write holds up the other writes is bounded by the limit of five seconds above.

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.
  • Added in 1.2Use 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.