Skip to content

Commit 483805c

Browse files
committed
feat: add appendRecords operation with consistency checks
1 parent 6f085c3 commit 483805c

19 files changed

Lines changed: 3440 additions & 6 deletions

docs/api/appending-events.md

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -247,13 +247,15 @@ client.appendToStream("some-stream", options, eventData)
247247
.get();
248248
```
249249

250-
## Append to multiple streams
250+
## Atomic appends
251251

252-
::: note
253-
This feature is only available in KurrentDB 25.1 and later.
254-
:::
252+
KurrentDB provides two operations for appending events to one or more streams in a single atomic transaction: `appendRecords` and `multiStreamAppend`. Both guarantee that either all writes succeed or the entire operation fails, but they differ in how records are organized, ordered, and validated.
255253

256-
You can append events to multiple streams in a single atomic operation. Either all streams are updated, or the entire operation fails.
254+
| | `appendRecords` | `multiStreamAppend` |
255+
|------------------------|-----------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
256+
| **Available since** | KurrentDB 26.1 | KurrentDB 25.1 |
257+
| **Record ordering** | Interleaved. Records from different streams can be mixed, and their exact order is preserved in the global log. | Grouped. All records for a stream are sent together; ordering across streams is not guaranteed. |
258+
| **Consistency checks** | Decoupled. Can validate the state of any stream, including streams not being written to. | Coupled. Expected state is specified per stream being written to. |
257259

258260
::: warning
259261
Metadata must be a valid JSON object, using string keys and string values only.
@@ -262,6 +264,88 @@ KurrentDB's metadata handling. This restriction will be lifted in the next major
262264
release.
263265
:::
264266

267+
### AppendRecords
268+
269+
::: note
270+
This feature is only available in KurrentDB 26.1 and later.
271+
:::
272+
273+
`appendRecords` appends events to one or more streams atomically. Each record specifies which stream it targets, and the exact order of records is preserved in the global log across all streams.
274+
275+
#### Single stream
276+
277+
The simplest usage appends events to a single stream:
278+
279+
```java
280+
EventData eventOne = EventData
281+
.builderAsJson("OrderPlaced", "{\"orderId\": \"123\"}".getBytes())
282+
.build();
283+
284+
EventData eventTwo = EventData
285+
.builderAsJson("OrderShipped", "{\"orderId\": \"123\"}".getBytes())
286+
.build();
287+
288+
client.appendRecords("order-123", Arrays.asList(eventOne, eventTwo)).get();
289+
```
290+
291+
When no expected state is provided, no consistency check is performed, which is equivalent to `StreamState.any()`.
292+
293+
You can also pass an expected stream state for optimistic concurrency:
294+
295+
```java
296+
client.appendRecords("order-123", StreamState.noStream(), Arrays.asList(eventOne, eventTwo)).get();
297+
```
298+
299+
#### Multiple streams
300+
301+
Use `AppendRecord` to target different streams. Records can be interleaved freely, and the global log preserves the exact order you specify:
302+
303+
```java
304+
List<AppendRecord> records = Arrays.asList(
305+
new AppendRecord("order-stream", EventData
306+
.builderAsJson("OrderCreated", "{\"orderId\": \"123\"}".getBytes())
307+
.build()),
308+
new AppendRecord("inventory-stream", EventData
309+
.builderAsJson("ItemReserved", "{\"itemId\": \"abc\", \"quantity\": 2}".getBytes())
310+
.build()),
311+
new AppendRecord("order-stream", EventData
312+
.builderAsJson("OrderConfirmed", "{\"orderId\": \"123\"}".getBytes())
313+
.build())
314+
);
315+
316+
client.appendRecords(records).get();
317+
```
318+
319+
#### Consistency checks
320+
321+
Consistency checks let you validate the state of any stream, including streams you are not writing to, before the append is committed. All checks are evaluated atomically: if any check fails, the entire operation is rejected and an `AppendConsistencyViolationException` is thrown with details about every failing check and the actual state observed.
322+
323+
```java
324+
List<AppendRecord> records = Collections.singletonList(
325+
new AppendRecord("order-stream", EventData
326+
.builderAsJson("OrderConfirmed", "{\"orderId\": \"123\"}".getBytes())
327+
.build())
328+
);
329+
330+
// ensure the inventory stream exists before confirming the order,
331+
// even though we are not writing to it
332+
List<ConsistencyCheck> checks = Collections.singletonList(
333+
new ConsistencyCheck.StreamStateCheck("inventory-stream", StreamState.streamExists())
334+
);
335+
336+
client.appendRecords(records, checks).get();
337+
```
338+
339+
Because checks are decoupled from writes, you can validate the state of streams you are not writing to, enabling patterns where a business decision depends on the state of multiple streams but the resulting event is written to only one of them.
340+
341+
### MultiStreamAppend
342+
343+
::: note
344+
This feature is only available in KurrentDB 25.1 and later.
345+
:::
346+
347+
`multiStreamAppend` appends events to one or more streams atomically. Records are grouped per stream using `AppendStreamRequest`, where each request specifies a stream name, an expected state, and the events for that stream.
348+
265349
```java
266350
JsonMapper mapper = new JsonMapper();
267351

@@ -293,6 +377,10 @@ List<AppendStreamRequest> requests = Arrays.asList(
293377
)
294378
);
295379

296-
MultiAppendWriteResult result = client.multiStreamAppend(requests.iterator()).get();
380+
MultiStreamAppendResponse result = client.multiStreamAppend(requests.iterator()).get();
297381
```
298382

383+
Each stream can only appear once in the request. The expected state is validated per stream before the transaction is committed.
384+
385+
The result returns the position of the last appended record in the transaction and a collection of responses for each stream.
386+

0 commit comments

Comments
 (0)