Bi-Temporal Indexing
EvidentSource provides bi-temporal indexing, allowing you to query your data across two time dimensions: when events occurred in the real world (effective time) and when they were recorded in the system (revision time). This powerful feature enables complex temporal queries essential for audit trails, compliance, and historical analysis.
The Two Time Dimensions
Section titled “The Two Time Dimensions”Effective Time (Business Time)
Section titled “Effective Time (Business Time)”Effective time represents when an event actually occurred in the real world:
- Stored in the CloudEvents
timeattribute - Represents the business reality
- Can be in the past, present, or even future
- May be corrected retroactively
Revision Time (System Time)
Section titled “Revision Time (System Time)”Revision time represents when an event was recorded in EvidentSource:
- Automatically assigned by the system
- Stored as
recordedtimeandrevisionnumber - Always moves forward (append-only)
- Cannot be changed once recorded
Why Bi-Temporal?
Section titled “Why Bi-Temporal?”Bi-temporal data modeling is crucial for:
Compliance and Auditing
Section titled “Compliance and Auditing”- Regulatory requirements: Many industries require both when something happened and when it was recorded
- Audit trails: Complete history of data changes and when they were made
- Data lineage: Track how information evolved over time
Retroactive Corrections
Section titled “Retroactive Corrections”- Late-arriving data: Record events that happened in the past
- Corrections: Fix errors while maintaining the original records
- Backdating: Handle scenarios where business events are recorded after they occur
Historical Analysis
Section titled “Historical Analysis”- Point-in-time queries: “What did we know at this point?”
- As-of queries: “What was the state as of this business date?”
- Temporal joins: Correlate data across different time dimensions
Temporal Query Patterns
Section titled “Temporal Query Patterns”Query by Revision (System Time)
Section titled “Query by Revision (System Time)”Query the database as it existed at a specific revision:
// Get database state at revision 1000let request = FetchDatabaseAtRevisionRequest { database_name: "mydb".into(), revision: 1000,};let database = client.fetch_database_at_revision(request).await?;
// Query events up to revision 1000let query = EventQuery { revision_range: Some(RevisionRange { start: 1, end: 1000 }), ..Default::default()};Query by Effective Time (Business Time)
Section titled “Query by Effective Time (Business Time)”Query events based on when they occurred in the real world:
// Get all events that occurred on a specific datelet query = EventQuery { effective_time_range: Some(TimeRange { start: Some(Timestamp::from_str("2024-01-01T00:00:00Z")?), end: Some(Timestamp::from_str("2024-01-02T00:00:00Z")?), }), ..Default::default()};
// Database state as of a business datelet request = DatabaseEffectiveAtTimestampRequest { database_name: "mydb".into(), timestamp: Some(Timestamp::from_str("2024-01-15T12:00:00Z")?),};let database = client.database_effective_at_timestamp(request).await?;Combined Temporal Queries
Section titled “Combined Temporal Queries”Query across both time dimensions:
// "What did we know on Jan 31st about events that occurred in January?"let query = EventQuery { // Business time: Events that occurred in January effective_time_range: Some(TimeRange { start: Some(Timestamp::from_str("2024-01-01T00:00:00Z")?), end: Some(Timestamp::from_str("2024-02-01T00:00:00Z")?), }), // System time: As recorded by Jan 31st revision_range: Some(RevisionRange { start: 1, end: revision_as_of_jan31, }), ..Default::default()};Real-World Examples
Section titled “Real-World Examples”Financial Transactions
Section titled “Financial Transactions”Consider a bank transaction that occurred on Friday but was processed on Monday:
{ "id": "txn-123", "type": "AccountDebit", "time": "2024-01-12T15:30:00Z", // Friday (effective time) "data": { "amount": 100.00, "account": "ACC-789" }}// Recorded on Monday with revision 5678// recordedtime: "2024-01-15T09:00:00Z"Queries:
- “Show Friday’s transactions” → Query by effective time
- “What transactions were in the system by Monday morning?” → Query by revision
- “What Friday transactions did we know about by end of day Friday?” → Combined query
Inventory Adjustments
Section titled “Inventory Adjustments”A warehouse discovers a counting error from last month:
{ "id": "adj-456", "type": "InventoryAdjustment", "time": "2024-01-05T10:00:00Z", // When the error occurred "subject": "SKU-123", "data": { "quantity": -50, "reason": "Counting error discovered" }}// Recorded today with revision 9012// recordedtime: "2024-02-15T14:30:00Z"This allows:
- Accurate historical inventory levels (effective time)
- Audit trail of when corrections were made (revision time)
- Analysis of how long it took to discover errors
Regulatory Reporting
Section titled “Regulatory Reporting”Generate reports showing what was known at specific points:
// Quarterly report: What we knew at quarter endlet q4_end_revision = get_revision_as_of("2023-12-31T23:59:59Z").await?;
// Get all events for Q4 as known at quarter endlet query = EventQuery { effective_time_range: Some(TimeRange { start: Some(Timestamp::from_str("2023-10-01T00:00:00Z")?), end: Some(Timestamp::from_str("2024-01-01T00:00:00Z")?), }), revision_range: Some(RevisionRange { start: 1, end: q4_end_revision, }), ..Default::default()};
let mut stream = client.query_events(QueryEventsRequest { database_name: "mydb".into(), query: Some(query),}).await?.into_inner();Temporal Consistency
Section titled “Temporal Consistency”Revision Consistency
Section titled “Revision Consistency”- Revisions are strictly increasing
- Each transaction gets a unique revision
- Queries at a revision see a consistent snapshot
Effective Time Considerations
Section titled “Effective Time Considerations”- Events can have any effective time
- No ordering enforced on effective time
- Late events don’t affect revision order
State View Temporality
Section titled “State View Temporality”State Views can leverage both dimensions:
// State view that considers both timesfn reduce(state: State, event: Event) -> State { match event.time { Some(effective_time) => { // Business logic based on when event occurred } None => { // Use recorded time as fallback } }}Best Practices
Section titled “Best Practices”Choose the Right Time
Section titled “Choose the Right Time”- User actions: Use effective time (when user clicked)
- System events: Consider using recorded time
- External events: Use effective time from source system
- Corrections: Always use original effective time
Handle Missing Times
Section titled “Handle Missing Times”Not all events have effective time:
let effective_time = event.time.or_else(|| { event.extensions.get("recordedtime").and_then(|v| parse_timestamp(v).ok())});Time Zone Considerations
Section titled “Time Zone Considerations”- Always use UTC for storage
- Convert to local time zones for display
- Include timezone context in event data if needed
Temporal Indexes
Section titled “Temporal Indexes”EvidentSource automatically maintains indexes for efficient temporal queries:
- Revision index: O(1) lookup by revision
- Time index: Efficient range queries by effective time
- Combined indexes: Optimized for bi-temporal queries
Advanced Patterns
Section titled “Advanced Patterns”Temporal Joins
Section titled “Temporal Joins”Join events across time dimensions:
// Find all orders and their payments as of month endlet month_end_revision = get_revision_as_of("2024-01-31T23:59:59Z").await?;
let orders = query_events( EventSelector { event_type: Some("OrderCreated".into()), ..Default::default() }, Some(RevisionRange { start: 0, end: month_end_revision }),).await?;
let payments = query_events( EventSelector { event_type: Some("PaymentReceived".into()), ..Default::default() }, Some(RevisionRange { start: 0, end: month_end_revision }),).await?;
// Join by subject (order ID)Temporal Aggregations
Section titled “Temporal Aggregations”Aggregate across time windows:
// Daily totals as known at day endasync fn daily_totals(date: NaiveDate) -> Result<HashMap<String, f64>> { let day_start = date.and_hms_opt(0, 0, 0).unwrap(); let day_end = date.and_hms_opt(23, 59, 59).unwrap(); let revision = get_revision_as_of(day_end).await?;
let events = query_events(QueryEventsRequest { effective_time_range: Some(TimeRange { start: Some(day_start.into()), end: Some(day_end.into()), }), revision_range: Some(RevisionRange { start: 0, end: revision }), ..Default::default() }).await?;
let mut totals: HashMap<String, f64> = HashMap::new(); for event in events { *totals.entry(event.ty.clone()).or_default() += calculate_total(&event); } Ok(totals)}Temporal Snapshots
Section titled “Temporal Snapshots”Create point-in-time snapshots:
// Snapshot of all active orders as of a specific timestruct Snapshot { effective_time: DateTime<Utc>, revision: u64, active_orders: Vec<Order>,}
async fn create_snapshot(effective_time: DateTime<Utc>) -> Result<Snapshot> { let revision = get_current_revision().await?; let active_orders = build_active_orders(effective_time, revision).await?;
Ok(Snapshot { effective_time, revision, active_orders })}Next Steps
Section titled “Next Steps”- Learn about State Views for temporal materialization
- Explore Event Sourcing patterns
- See API Reference for temporal query APIs
- Understand Architecture of temporal indexes