The trogon.consistency.v1alpha1 package provides Protobuf message definitions for implementing read-after-write consistency in eventual consistency systems, particularly CQRS/Event Sourcing architectures.
In CQRS/Event Sourcing systems, there's often a delay (projection lag) between:
- Writing an event to the event store
- The read model/projection processing that event
This creates a race condition where a client performs a mutation but immediately queries and gets stale data (doesn't see their own write).
The Consistency message allows clients to specify version requirements for queries, enabling the server to retry until the projection catches up.
Main message with two modes:
message Consistency {
oneof requirement {
MinVersion min_version = 1;
ExactVersion exact_version = 2;
}
optional google.protobuf.Duration timeout_duration = 3;
optional google.protobuf.Duration delay_duration = 4;
}Wait for projection to reach at least the specified version (version >= X).
Use for: Read-after-write consistency (most common use case)
message MinVersion {
int64 version = 1;
}Wait for projection to reach exactly the specified version (version == X).
Use for: Reproducible queries, audits, point-in-time reports
message ExactVersion {
int64 version = 1;
}// Example mutation response
message CreateOrderResponse {
string order_id = 1;
uint64 stream_version = 2; // ← Returns current event stream version
}// Example query request
message GetOrderRequest {
string order_id = 1;
trogon.consistency.v1alpha1.Consistency consistency = 2;
}- Perform mutation and capture
stream_versionfrom response (e.g., version 5) - Immediately query with consistency requirement:
- Set
min_version.versionto the captured version - Configure
timeout_duration(e.g., 1s) - Configure
delay_duration(e.g., 100ms)
- Set
- Server retries until projection catches up or timeout
- Client receives result guaranteed to include their write
| Feature | MinVersion | ExactVersion |
|---|---|---|
| Read-Your-Writes | Yes | Yes |
| Returns Newer Data | Yes | Fails (FAILED_PRECONDITION) |
| Use Case | Normal queries | Audits, reports |
| Flexibility | High | Low |
| Common | 99% of cases | 1% of cases |
Servers should enforce reasonable limits:
-
timeout_duration:
- Default: 1s (if not provided)
- Maximum: 5s (clamp values above)
-
delay_duration:
- Default: 100ms (if not provided)
- Maximum: 500ms (clamp values above)
When projection fails to meet consistency requirements, return appropriate gRPC status codes:
Projection Timeout (timeout exceeded):
- Status Code:
UNAVAILABLE(503) - Use
google.rpc.ErrorInfofor structured error details - Suggested metadata: min_version, current_version, attempts, elapsed_ms
Snapshot Expired (ExactVersion only, version moved past):
- Status Code:
FAILED_PRECONDITION(400) - Use
google.rpc.ErrorInfofor structured error details - Suggested metadata: requested_version, current_version
Consider using google.rpc.Status with google.rpc.ErrorInfo for rich error responses that clients can programmatically handle.
1. Execute query
2. Check projection version
3. If version requirement met → return result
4. If ExactVersion and version > required → return FAILED_PRECONDITION
5. If timeout exceeded → return UNAVAILABLE
6. Sleep for delay_duration
7. Goto step 1
If query returns NOT_FOUND (entity doesn't exist in projection yet), treat it as projection lag and retry until timeout. The entity might appear once projection catches up.
Track these metrics:
consistency.attempts- Distribution of retry attemptsconsistency.latency- Time to satisfy consistency requirementconsistency.timeouts- Count of timeoutsconsistency.snapshot_expired- Count of ExactVersion failures
Log when:
- Clamping client-provided timeout/delay values
- Timeout exceeded
- Snapshot expired (ExactVersion)
- Retry attempts (at debug level)
Add span attributes:
consistency.mode- "min_version" or "exact_version"consistency.required_version- Requested versionconsistency.actual_version- Projection version when satisfiedconsistency.attempts- Number of retriesconsistency.elapsed_ms- Time taken
- Enables read-after-write consistency
- No changes to event store required
- Client controls consistency vs latency trade-off
- Works with any projection technology
- Increases query latency (retry loops)
- Higher server load (repeated queries)
- Client must track and pass versions
- Doesn't work with projections that don't track version
Use Consistency for:
- Mutations followed by immediate reads (e.g., "Create order → View order")
- User flows requiring strong consistency UX
- Testing/demos where eventual consistency is confusing
Don't use Consistency for:
- Browsing/listing queries (eventual consistency is fine)
- Public-facing queries (projection lag usually acceptable)
- High-throughput read-heavy workloads (adds latency)
Inspired by:
-
SpiceDB:
at_least_as_freshandat_exact_snapshotmodes -
DynamoDB: Consistent reads
-
Cassandra: QUORUM/ALL consistency levels