Skip to content

Bitemporal Persistence

Bitemporal persistence is a data management approach that tracks both valid time (when a fact is true in the real world) and transaction time (when a fact is stored in the database). This allows for historical queries, auditing, and correction of past data without losing information about previous states.

TimeCoordinates represents a pair of timestamps:

@Serializable
data class TimeCoordinates(val effective: Long, val recorded: Long)
  • Effective Time (Valid Time): When a fact is true in the real world. The time period during which the information is considered valid.
  • Recorded Time (Transaction Time): When a fact was recorded in the system. The history of how the system’s knowledge of the real world evolved.

All bitemporal records are associated with a TimeCoordinates instance, enabling queries as of any point in both timelines.

BitemporalTable abstracts a table with columns for:

  • eId — entity identity (UUID)
  • rId — record identity (version identifier, UUID)
  • effectiveAsOf — when this fact was effective in the real world
  • recordedAsOf — when this fact was recorded in the system
  • author — who made this change
  • previous — reference to the previous record in the lineage (UUID, nullable)
  • retired — logical delete flag

TimeCoordinatesColumn is a composite column for storing and retrieving TimeCoordinates pairs.

BitemporalEntity is a serializable data class representing a versioned entity:

abstract class BitemporalRecord<EP, M, TBL, SELF> {
val rId: EntityID<UUID>
var eId by table.eId
var effectiveAsOf by table.effectiveAsOf
var recordedAsOf by table.recordedAsOf
var retired by table.retired
var previous by table.previous
var author by table.author
var payload: EP
var metadata: M
}
  • CreateGuard, UpdateGuard, DeleteGuard: Type aliases for suspend functions that enforce business rules before operations.
  • Idempotency: Enum controlling how updates handle duplicate or conflicting changes:
    • REJECT: Reject the duplicate
    • CONFIRM: Treat as confirmed (update succeeds with existing state)
    • SKIP: Silently skip duplicates

The Universe interface defines the contract for bitemporal persistence:

  • create(payload, metadata, asOf, author): Creates a new bitemporal entity. The asOf parameter specifies both effective and recorded time.
  • read(eId, asOf): Retrieves the entity record valid at the specified effective time and recorded at or before the specified recorded time.
  • readRecord(rId): Retrieves a specific historical record by record ID.
  • update(update): Creates a new record representing the updated state, with new effectiveAsOf and recordedAsOf timestamps, linking to the previous record.
  • delete(originEId, metadata, asOf, author): Creates a new record marked as retired = true.

All operations return DBIO<T> — see Functional Programming: DBIO.

Provides Exposed-based implementations for:

  • Reading the latest entity as-of a given time
  • Listing all latest entities for each unique ID (using subquery or window function strategies)
  • Counting entities as-of a given time
  • Ensuring uniqueness and enforcing idempotency/versioning rules
  • Full auditability and historical reconstruction
  • Correction of past data without data loss
  • Support for complex business rules and versioning strategies
  • “As-of” querying on either or both time dimensions