Append Condition
An Append Condition makes a write fail if something relevant happened since the client read. It’s how a decision is protected against concurrent writes.
Key words in capitals follow RFC 2119.
The flow
TamarackDB runs the standard DCB flow, optimistically:
- Read the relevant events, and keep the Sequence Position read up to (
afterSequence) and the store ID. - Decide on the new events to append.
- Write them with a condition: fail if an event matching the query exists after
afterSequence. - If another write appended a matching event in between, the write gets
409 ConcurrencyExceptionand nothing is written. Read again, decide again, and send a new write.
A read never locks anything. The decision is made on what was read, and the write carries what the decision depends on.
Shape
{
"failIfEventsMatch": [
{ "identifiers": [ { "name": "userId", "value": "123" } ] }
],
"afterSequence": 12345,
"store": "5b0c7e2a-1f4d-4a9b-8c3e-6d2f1a0b9e47"
}failIfEventsMatch: a query, in the same grammar as a read.afterSequence: the Sequence Position the client read up to. It can be past the last matching event: it’s what the client saw, not necessarily a real event.store: the store ID that read returned (see Store ID).
Rules
- The condition fails if any event matching
failIfEventsMatchhas a Sequence Position greater thanafterSequence. - A condition with
afterSequenceMUST carrystore. A condition withoutafterSequenceMUST NOT carry it: it read nothing, so it holds on any store. Breaking either rule gets400. - A condition whose
storeisn’t the current store ID fails with409: itsafterSequencenames a position in another history. The server decides this without any SQL. - Both
failIfEventsMatchandafterSequenceare optional:failIfEventsMatchalone fails if any matching event exists at all, from Sequence Position 1. It suits a decision that rests on no read, for example “fail if auser-registeredevent with this email exists”.afterSequencealone fails if any event at all exists after it.- A condition with neither field always holds: it says nothing, so it protects nothing. A rewrite MUST NOT read an
empty condition as
afterSequence: 0, which would fail as soon as the store holds one event.
- A write carries a list of conditions, at most
maxEventsPerWriteof them, and every one MUST hold. The first one that fails ends the write, and the409names it by its place:conditions[1] no longer holds, orconditions[0] was read on another store. - The server checks conditions against committed events only, in the same SQLite transaction as the inserts. Nothing can slip in between the checks and the inserts (see The write FIFO).
Why a list. A transaction usually has one condition per decision: each decision model or processor adds the condition its own read supports. Merging them into one condition with OR would refuse a write whenever any of the queries matched anything after the earliest position. A list is more precise, and still never a partial success.
What a condition doesn’t cover
- Projections. A decision rests on events, never on a projection. A write checks the projections it changes, by their version, and nothing else. A projection the transaction only read isn’t checked: the server never learns what a transaction read, and a projection can be stale the moment it’s read.
- Pending events of the same transaction. A decision can also go stale because of a pending event added after its read, by another processor of the same command for example. Only the client library knows the order of its reads and pending events, so that check is the library’s (see Client libraries).
- A condition left out, or too narrow. It leaves a race that no error reports. Describing what a decision depends on is the application’s job.
DCB compliance
TamarackDB follows the DCB specification:
| Requirement | Level | TamarackDB |
|---|---|---|
| Read events filtered by type and/or tags through a Query | MUST | QUERY /events |
| Read from a given Sequence Position | SHOULD | afterSequence on QUERY /events |
| Append one or more events atomically | MUST | Every
POST /write commits atomically |
| Fail the append if an event matches the Append Condition, when one is given | MUST | conditions on POST /write, optional for the client |
Its guarantee is exactly the one of the specification, no more and no less. Only what an Append Condition or a projection version expresses is protected. A broader guarantee is a business rule of the application, not something the event store enforces.