Skip to content

Commit 39afbc1

Browse files
committed
added bookmarking topic on projector docs
1 parent eb015b9 commit 39afbc1

File tree

1 file changed

+145
-1
lines changed

1 file changed

+145
-1
lines changed

_posts/2025-11-29-eventstore-projecting-events.md

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,4 +345,148 @@ Accumulated metrics are useful for:
345345
- **Monitoring**: Track total events processed over time
346346
- **Debugging**: Identify performance issues across runs
347347
- **Resumption**: Get the current position for checkpointing
348-
- **Reporting**: Calculate processing statistics
348+
- **Reporting**: Calculate processing statistics
349+
350+
## Bookmarking for Process Restart and Progress Tracking
351+
352+
Bookmarking allows projectors to automatically save and restore their position in the event stream. This enables projectors to:
353+
354+
- **Resume after restart**: Pick up exactly where they left off when your application restarts
355+
- **Track progress**: Monitor how far a projection has processed through the event stream
356+
- **Allow projection updates to run subsequently on different instances**: Share position with next application instance needing it
357+
- **Enable incremental updates**: Process only new events since the last run (without relying on a local in-process variable)
358+
359+
A bookmark consists of a reader name (unique identifier) and an event reference (position in the stream).
360+
Optionally, you can add tags to add metadata (eg: application version, hostname, process id, ... that placed the bookmark)
361+
362+
### Basic Bookmarking
363+
364+
Configure bookmarking using the fluent API:
365+
366+
```java
367+
CustomerSummary projection = new CustomerSummary("123");
368+
369+
Projector<CustomerEvent> projector = Projector.from(stream)
370+
.towards(projection)
371+
.bookmarkProgress()
372+
.withReader("customer-summary-projection")
373+
.done()
374+
.build();
375+
376+
// First run processes all historical events and saves bookmark
377+
projector.run();
378+
379+
// Application restarts...
380+
381+
// Second run automatically reads bookmark and processes only new events
382+
projector.run();
383+
```
384+
385+
The projector automatically:
386+
1. Reads the bookmark before each run to determine the starting position
387+
2. Processes events from that position forward
388+
3. Saves the updated bookmark after processing new events
389+
390+
### Continuous Projection Updates
391+
392+
For long-running projections that need to stay current with new events, combine bookmarking with event notifications:
393+
394+
```java
395+
public class CustomerProjectionService {
396+
private final EventStream<CustomerEvent> stream;
397+
private final CustomerSummary projection;
398+
private final Projector<CustomerEvent> projector;
399+
400+
public CustomerProjectionService(EventStore eventStore) {
401+
EventStreamId streamId = EventStreamId.forContext("customers");
402+
this.stream = eventStore.getEventStream(streamId, CustomerEvent.class);
403+
this.projection = new CustomerSummary("123");
404+
405+
// Configure projector with bookmarking
406+
this.projector = Projector.from(stream)
407+
.towards(projection)
408+
.bookmarkProgress()
409+
.withReader("customer-summary")
410+
.withTags(Tags.of("customer", "123"))
411+
.readBeforeEachExecution()
412+
.done()
413+
.build();
414+
415+
// Subscribe to append notifications
416+
stream.subscribe(this::updateProjection);
417+
}
418+
419+
/*
420+
* This method is called asynchronously each time the
421+
*/
422+
private void updateProjection(EventReference atLeastUntil) {
423+
424+
// Project new events, bookmark is fetched before this run (ref builder instructions above)
425+
ProjectorMetrics metrics = projector.run();
426+
427+
if (metrics.eventsHandled() > 0) {
428+
System.out.println("Processed " + metrics.eventsHandled() + " new events");
429+
System.out.println("Current position: " + metrics.lastEventReference());
430+
}
431+
432+
// Bookmark is automatically updated after each run
433+
}
434+
435+
public CustomerSummary getProjection() {
436+
return projection;
437+
}
438+
}
439+
```
440+
441+
This pattern ensures your read model stays synchronized with the event stream:
442+
443+
1. **Initial state**: The projector reads the bookmark to resume from the last known position
444+
2. **New events**: When events are appended, the notification triggers an update
445+
3. **Incremental processing**: Only new events since the bookmark are processed
446+
4. **Automatic bookmark update**: The new position is saved for the next run
447+
448+
### Bookmark Read Frequencies
449+
450+
Control when bookmarks are read to match your use case:
451+
452+
```java
453+
// Read before each execution (default) - for distributed systems
454+
Projector.from(stream)
455+
.towards(projection)
456+
.bookmarkProgress()
457+
.withReader("my-projection")
458+
.readBeforeEachExecution() // Default behavior
459+
.done()
460+
.build();
461+
462+
// Read once at creation - for single-instance applications
463+
Projector.from(stream)
464+
.towards(projection)
465+
.bookmarkProgress()
466+
.withReader("my-projection")
467+
.readAtCreationOnly()
468+
.done()
469+
.build();
470+
471+
// Read before first execution - for delayed initialization
472+
Projector.from(stream)
473+
.towards(projection)
474+
.bookmarkProgress()
475+
.withReader("my-projection")
476+
.readBeforeFirstExecution()
477+
.done()
478+
.build();
479+
480+
// Manual control - explicit bookmark management
481+
Projector<CustomerEvent> projector = Projector.from(stream)
482+
.towards(projection)
483+
.bookmarkProgress()
484+
.withReader("my-projection")
485+
.readOnManualTriggerOnly()
486+
.done()
487+
.build();
488+
489+
// Explicitly read bookmark when needed
490+
projector.readBookmark();
491+
projector.run();
492+
```

0 commit comments

Comments
 (0)