Skip to content

Commit c5d9d8c

Browse files
committed
elaborated EventQuery docs
1 parent 39afbc1 commit c5d9d8c

File tree

1 file changed

+367
-1
lines changed

1 file changed

+367
-1
lines changed

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

Lines changed: 367 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,153 @@ tags: [events,query,tags]
1010

1111
This guide covers the various ways to query events from the EventStore, including filtering, pagination, backward queries, temporal queries, and cross-stream querying.
1212

13+
## EventQuery Concept
14+
15+
An `EventQuery` is the fundamental mechanism for selecting events from the EventStore. It defines matching criteria based on event types and tags.
16+
17+
**An event matches an EventQuery if:**
18+
1. The event's type is **any** of the types allowed by the query (OR condition)
19+
2. **All** tags specified by the query are present on the event (AND condition)
20+
21+
Events may have additional tags beyond those specified in the query—the query only requires that the specified tags are present.
22+
23+
```java
24+
// Query for CustomerRegistered OR CustomerUpdated events with region=EU tag
25+
EventQuery query = EventQuery.forEvents(
26+
EventTypesFilter.of(CustomerRegistered.class, CustomerUpdated.class),
27+
Tags.of("region", "EU")
28+
);
29+
30+
// This matches:
31+
Event.of(new CustomerRegistered("John"), Tags.of("region", "EU")) // ✓ Type matches, has required tag
32+
Event.of(new CustomerUpdated("Jane"), Tags.of("region", "EU", "premium", "true")) // ✓ Type matches, has required tag (plus extra)
33+
34+
// This does NOT match:
35+
Event.of(new CustomerRegistered("Bob"), Tags.of("region", "US")) // ✗ Type matches, but wrong tag value
36+
Event.of(new CustomerChurned("Alice"), Tags.of("region", "EU")) // ✗ Has required tag, but wrong type
37+
Event.of(new CustomerUpdated("Dave"), Tags.none()) // ✗ Type matches, but missing required tag
38+
```
39+
40+
The same `EventQuery` object can be used both for database-level filtering and in-process filtering, as explained in the next section.
41+
42+
## In-Database vs In-Process Querying
43+
44+
The `EventQuery` object is versatile—it can be used to filter events at two different levels:
45+
46+
1. **Database-level filtering**: Pass the query to the event stream's `query()` method
47+
2. **In-process filtering**: Use the query's `matches()` method in your Java code
48+
49+
### Database-Level Filtering (Recommended)
50+
51+
When you pass an `EventQuery` to the event stream, the filtering happens in the event storage (database):
52+
53+
```java
54+
EventStream<CustomerEvent> stream = eventstore.getEventStream(streamId, CustomerEvent.class);
55+
56+
EventQuery query = EventQuery.forEvents(
57+
EventTypesFilter.of(CustomerRegistered.class),
58+
Tags.of("region", "EU")
59+
);
60+
61+
// Query is executed in the database
62+
Stream<Event<CustomerEvent>> events = stream.query(query);
63+
events.forEach(event -> processEvent(event));
64+
```
65+
66+
**Advantages:**
67+
- Only matching events are read from the database
68+
- Efficient—leverages database indexes and query optimization
69+
- Minimal memory usage and network transfer
70+
- Recommended for most use cases
71+
72+
### In-Process Filtering
73+
74+
The same `EventQuery` object can filter events in your Java application:
75+
76+
```java
77+
EventStream<CustomerEvent> stream = eventstore.getEventStream(streamId, CustomerEvent.class);
78+
79+
EventQuery query = EventQuery.forEvents(
80+
EventTypesFilter.of(CustomerRegistered.class),
81+
Tags.of("region", "EU")
82+
);
83+
84+
// Query all events from database, filter in Java
85+
Stream<Event<CustomerEvent>> allEvents = stream.query(EventQuery.matchAll());
86+
Stream<Event<CustomerEvent>> filtered = allEvents.filter(query::matches);
87+
filtered.forEach(event -> processEvent(event));
88+
```
89+
90+
**Disadvantages:**
91+
- All events are read from the database
92+
- Filtering happens in application memory
93+
- Poor performance with large event streams
94+
- Higher memory usage and network transfer
95+
96+
**Important:** While these two approaches are functionally equivalent (they return the same events), the in-process approach suffers from significant performance issues because all events must be retrieved from the database before filtering.
97+
98+
### Hybrid Approach: Coarse Database Filtering + Fine-Grained In-Process Filtering
99+
100+
Sometimes it's beneficial to retrieve a limited set of events from the database and then apply multiple fine-grained filters in Java. This allows you to reuse query results for multiple objectives without running multiple similar database queries:
101+
102+
```java
103+
EventStream<CustomerEvent> stream = eventstore.getEventStream(streamId, CustomerEvent.class);
104+
105+
// Coarse filter: Get all customer events for EU region
106+
EventQuery broadQuery = EventQuery.forEvents(
107+
EventTypesFilter.any(), // All event types
108+
Tags.of("region", "EU")
109+
);
110+
111+
List<Event<CustomerEvent>> euEvents = stream.query(broadQuery).toList();
112+
113+
// Now apply multiple fine-grained filters in-process
114+
EventQuery registrationsQuery = EventQuery.forEvents(
115+
EventTypesFilter.of(CustomerRegistered.class),
116+
Tags.of("region", "EU")
117+
);
118+
119+
EventQuery premiumQuery = EventQuery.forEvents(
120+
EventTypesFilter.any(),
121+
Tags.of("region", "EU", "premium", "true")
122+
);
123+
124+
EventQuery churnQuery = EventQuery.forEvents(
125+
EventTypesFilter.of(CustomerChurned.class),
126+
Tags.of("region", "EU")
127+
);
128+
129+
// Reuse the same event list with different filters
130+
List<Event<CustomerEvent>> registrations = euEvents.stream()
131+
.filter(e -> registrationsQuery.matches(e))
132+
.toList();
133+
134+
List<Event<CustomerEvent>> premiumCustomers = euEvents.stream()
135+
.filter(e -> premiumQuery.matches(e))
136+
.toList();
137+
138+
List<Event<CustomerEvent>> churned = euEvents.stream()
139+
.filter(e -> churnQuery.matches(e))
140+
.toList();
141+
142+
System.out.println("EU Registrations: " + registrations.size());
143+
System.out.println("EU Premium: " + premiumCustomers.size());
144+
System.out.println("EU Churned: " + churned.size());
145+
```
146+
147+
**When to use this approach:**
148+
- You need to apply multiple related queries to the same dataset
149+
- The coarse query retrieves a manageable number of events
150+
- You want to avoid multiple database round-trips
151+
- Fine-grained filtering logic is complex or changes frequently
152+
153+
**When to avoid:**
154+
- The coarse query returns too many events (memory concerns)
155+
- You only need one specific filter (use database-level filtering instead)
156+
157+
This hybrid approach can balance efficiency and flexibility by retrieving a relevant subset once and filtering it multiple ways in memory,
158+
as long as you make sure the number of retrieved events is low enough to do so, or if you query in batches (see further)
159+
13160
## Querying all Domain Events in a Stream
14161

15162
The simplest query retrieves all events from a stream using `EventQuery.matchAll()`:
@@ -346,4 +493,223 @@ registrations.forEach(event -> {
346493
- Queries use **current event types** only
347494
- Historical events matching the upcasted target type are automatically included
348495
- The upcasting is transparent—application code never sees historical event types
349-
- No special handling needed in query logic for legacy events
496+
- No special handling needed in query logic for legacy events
497+
498+
## Complex Event Queries
499+
500+
For advanced scenarios, you can combine multiple query criteria using the `combineWith()` method. This creates a UNION of queries, allowing you to retrieve events that match any of several different patterns.
501+
502+
### Understanding Query Matching Semantics
503+
504+
Event queries follow specific matching rules that combine AND and OR logic:
505+
506+
**Within a single query item:**
507+
- **Event Types**: The event must match **ANY** of the specified types (OR condition)
508+
- **Tags**: The event must contain **ALL** specified tags (AND condition)
509+
510+
**Across multiple query items:**
511+
- If **ANY** item matches, the event matches the overall query (OR condition)
512+
513+
This gives you powerful flexibility to express complex selection criteria.
514+
515+
### Basic Query Combination
516+
517+
Combine two queries to match events that satisfy either query:
518+
519+
```java
520+
// Query 1: All CustomerRegistered events
521+
EventQuery newCustomers = EventQuery.forEvents(
522+
EventTypesFilter.of(CustomerRegistered.class),
523+
Tags.none()
524+
);
525+
526+
// Query 2: All events for VIP customers
527+
EventQuery vipActivity = EventQuery.forEvents(
528+
EventTypesFilter.any(),
529+
Tags.of("customerType", "VIP")
530+
);
531+
532+
// Combined: CustomerRegistered events OR any VIP customer events
533+
EventQuery combined = newCustomers.combineWith(vipActivity);
534+
535+
Stream<Event<CustomerEvent>> events = stream.query(combined);
536+
```
537+
538+
The combined query will return:
539+
- All `CustomerRegistered` events (regardless of tags)
540+
- All events (any type) with the tag `customerType=VIP`
541+
542+
No duplicates are returned (Events matching multiple items in the complex query are returned once.
543+
As always, events are returend in order of their position in the stream.
544+
545+
### Combining Queries with Different Types and Tags
546+
547+
Create complex selection criteria by combining queries with different event types and tag requirements:
548+
549+
```java
550+
// Events related to a specific student
551+
EventQuery studentEvents = EventQuery.forEvents(
552+
EventTypesFilter.of(StudentRegistered.class, StudentSubscribedToCourse.class),
553+
Tags.of("student", "S123")
554+
);
555+
556+
// Events related to a specific course
557+
EventQuery courseEvents = EventQuery.forEvents(
558+
EventTypesFilter.of(CourseDefined.class, CourseCapacityUpdated.class, StudentSubscribedToCourse.class),
559+
Tags.of("course", "CS101")
560+
);
561+
562+
// Combined: All events relevant to this student-course interaction
563+
EventQuery relevantFacts = studentEvents.combineWith(courseEvents);
564+
```
565+
566+
The combined query matches events where **any** of these conditions are true:
567+
- Event is `StudentRegistered` OR `StudentSubscribedToCourse` AND has tag `student=S123`
568+
- Event is `CourseDefined` OR `CourseCapacityUpdated` OR `StudentSubscribedToCourse` AND has tag `course=CS101`
569+
570+
Notice that `StudentSubscribedToCourse` events with **either** tag will be included.
571+
572+
### Query Combination Rules
573+
574+
When combining queries, certain rules apply:
575+
576+
**Compatible "until" references:**
577+
578+
```java
579+
// Both queries without "until" - OK
580+
EventQuery q1 = EventQuery.forEvents(EventTypesFilter.of(CustomerRegistered.class), Tags.none());
581+
EventQuery q2 = EventQuery.forEvents(EventTypesFilter.of(OrderPlaced.class), Tags.none());
582+
EventQuery combined = q1.combineWith(q2); // Success
583+
584+
// Both queries with same "until" - OK
585+
EventReference checkpoint = EventReference.of(someId, 100L);
586+
EventQuery q3 = EventQuery.forEvents(EventTypesFilter.of(CustomerRegistered.class), Tags.none())
587+
.until(checkpoint);
588+
EventQuery q4 = EventQuery.forEvents(EventTypesFilter.of(OrderPlaced.class), Tags.none())
589+
.until(checkpoint);
590+
EventQuery combinedHistorical = q3.combineWith(q4); // Success
591+
592+
// Different "until" references - ERROR
593+
EventQuery q5 = EventQuery.forEvents(EventTypesFilter.of(CustomerRegistered.class), Tags.none())
594+
.until(EventReference.of(someId, 100L));
595+
EventQuery q6 = EventQuery.forEvents(EventTypesFilter.of(OrderPlaced.class), Tags.none())
596+
.until(EventReference.of(otherId, 200L));
597+
// q5.combineWith(q6) throws IllegalArgumentException
598+
```
599+
600+
Both queries must have:
601+
- No "until" reference, **or**
602+
- The same "until" reference
603+
604+
Attempting to combine queries with different "until" references throws an `IllegalArgumentException`.
605+
606+
### Practical Use Case: Dynamic Consistency Boundary
607+
608+
Query combination is particularly useful for Dynamic Consistency Boundaries where business decisions depend on multiple types of facts:
609+
610+
```java
611+
public class SubscribeToCourseCommand {
612+
private final String studentId;
613+
private final String courseId;
614+
615+
public EventQuery relevantFacts() {
616+
// Query for student-specific facts
617+
EventQuery studentQuery = EventQuery.forEvents(
618+
EventTypesFilter.of(StudentRegistered.class, StudentSubscribedToCourse.class),
619+
Tags.of("student", studentId)
620+
);
621+
622+
// Query for course-specific facts
623+
EventQuery courseQuery = EventQuery.forEvents(
624+
EventTypesFilter.of(CourseDefined.class, CourseCapacityUpdated.class, StudentSubscribedToCourse.class),
625+
Tags.of("course", courseId)
626+
);
627+
628+
// Combine to get all relevant facts for this business decision
629+
return studentQuery.combineWith(courseQuery);
630+
}
631+
632+
public void execute(EventStream<LearningEvent> stream) {
633+
// Load current state based on relevant facts
634+
EventQuery query = relevantFacts();
635+
List<Event<LearningEvent>> facts = stream.query(query).toList();
636+
637+
// Make business decision based on facts
638+
CourseAggregate course = buildCourseState(facts);
639+
if (course.hasCapacity()) {
640+
EventReference lastRelevantFact = facts.getLast().reference();
641+
642+
// Append new event with optimistic locking
643+
stream.append(
644+
AppendCriteria.of(query, Optional.of(lastRelevantFact)),
645+
Event.of(
646+
new StudentSubscribedToCourse(studentId, courseId),
647+
Tags.of("student", studentId, "course", courseId)
648+
)
649+
);
650+
}
651+
}
652+
}
653+
```
654+
655+
This pattern ensures that if **any** new relevant fact emerges (either about the student or the course) between reading facts and appending the new event, the append will fail with an `OptimisticLockingException`.
656+
657+
### Matching Examples
658+
659+
To clarify the matching semantics, consider these examples:
660+
661+
**Example 1: Simple combination**
662+
663+
```java
664+
EventQuery q = EventQuery.forEvents(
665+
EventTypesFilter.of(CustomerRegistered.class),
666+
Tags.of("region", "EU")
667+
).combineWith(
668+
EventQuery.forEvents(
669+
EventTypesFilter.of(OrderPlaced.class),
670+
Tags.of("priority", "high")
671+
)
672+
);
673+
```
674+
675+
This matches events where:
676+
- Event type is `CustomerRegistered` AND has tag `region=EU`, **OR**
677+
- Event type is `OrderPlaced` AND has tag `priority=high`
678+
679+
**Example 2: Multiple types and tags per item**
680+
681+
```java
682+
EventQuery q = EventQuery.forEvents(
683+
EventTypesFilter.of(CustomerRegistered.class, CustomerUpdated.class),
684+
Tags.of("region", "EU", "verified", "true")
685+
).combineWith(
686+
EventQuery.forEvents(
687+
EventTypesFilter.of(OrderPlaced.class, OrderShipped.class),
688+
Tags.of("priority", "high")
689+
)
690+
);
691+
```
692+
693+
This matches events where:
694+
- Event type is `CustomerRegistered` OR `CustomerUpdated` AND has BOTH tags `region=EU` and `verified=true`, **OR**
695+
- Event type is `OrderPlaced` OR `OrderShipped` AND has tag `priority=high`
696+
697+
**Example 3: Any type with specific tags**
698+
699+
```java
700+
EventQuery q = EventQuery.forEvents(
701+
EventTypesFilter.any(),
702+
Tags.of("correlationId", "abc-123")
703+
).combineWith(
704+
EventQuery.forEvents(
705+
EventTypesFilter.of(ErrorOccurred.class),
706+
Tags.none()
707+
)
708+
);
709+
```
710+
711+
This matches events where:
712+
- ANY event type with tag `correlationId=abc-123`, **OR**
713+
- Event type is `ErrorOccurred` (regardless of tags)
714+
715+
This pattern is useful for debugging: retrieve all events in a specific correlation chain plus any error events.

0 commit comments

Comments
 (0)