@@ -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