Back to flin
flin

Every Entity Remembers Everything: The Temporal Model

How we designed FLIN's temporal model so every entity automatically tracks its complete history -- zero configuration, zero boilerplate, full time-travel out of the box.

Thales & Claude | March 25, 2026 10 min flin
flintemporalversioningmemory-native

Most programming languages treat data as disposable. You update a record, and the old value vanishes. You delete a row, and it is gone forever. If you want history, you build it yourself: audit tables, changelog middleware, event sourcing frameworks, version columns with triggers. Hundreds of lines of infrastructure code before you can answer the simplest question: "What was this value yesterday?"

We decided FLIN would be different. Every entity, from the moment it is created, remembers every state it has ever been in. Not as an opt-in feature. Not as a plugin. As the fundamental nature of data in the language itself.

This is the story of how we designed and built FLIN's temporal model -- the philosophy behind it, the architecture that makes it work, and why we believe "memory-native" is the future of application development.

The Problem We Kept Seeing

Before FLIN, every application Thales built at ZeroSuite ran into the same pattern. An accounting client would call: "The balance was wrong last Tuesday, but now it is different -- what happened?" A teacher on Deblo would report: "My student's grade changed and I do not know when." A user on any platform would accidentally overwrite data and have no way to recover it.

The solution was always the same: build an audit trail. Add a _history table. Write triggers. Wire up middleware. Test the middleware. Discover the middleware misses edge cases. Fix the edge cases. Repeat for every entity in the system.

After building audit trails for the fifth time, the pattern became obvious. This should not be application-level code. This should be infrastructure. Better yet, it should be the language itself.

The Design Principle: Zero Configuration Temporal

The core design principle was radical simplicity. If you define an entity in FLIN, it tracks history. Period. No annotations. No configuration. No opt-in.

entity User {
    name: text
    email: text
}

user = User { name: "Juste", email: "[email protected]" } save user

user.name = "Juste Gnimavo" save user

// Both versions exist. Always. current_name = user.name // "Juste Gnimavo" old_name = (user @ -1).name // "Juste" ```

That is the entire API. Define your entity. Save it. Update it. Save it again. Every previous state is preserved and queryable. The developer writes zero additional code. There is no @Versioned annotation, no enable_history() call, no migration to add a history table. The language handles it.

We called this "memory-native" because memory is not a feature bolted onto FLIN's data model -- it is the data model. Just as garbage-collected languages freed developers from manual memory management, FLIN's temporal model frees developers from manual history management.

How Automatic Versioning Works

Every save operation creates a new version. The runtime maintains a version timeline for each entity instance:

Version 0         Version 1         Version 2         Current
-------------------------------------------------------------------------->

+------------+ +------------+ +------------+ +------------+ | price: 10 |-->| price: 12 |-->| price: 15 |-->| price: 15 | | 10:00 AM | | 2:00 PM | | 5:00 PM | | (now) | +------------+ +------------+ +------------+ +------------+

product @ -3 product @ -2 product @ -1 product ```

Each version stores the complete field state plus metadata: a version number, a timestamp, and the entity ID. Previous versions are immutable -- you cannot modify history, only read it.

The implementation uses a bitemporal storage model. Each version has a valid_from timestamp (when this version became active) and a valid_to timestamp (when it was superseded). The current version has valid_to = NULL. This structure makes time-range queries efficient: to find the state at any given moment, the runtime searches for the version whose valid_from is at or before the target time and whose valid_to is after it (or NULL).

// How `user @ yesterday` translates to a database query
SELECT * FROM users
WHERE id = ?
  AND valid_from <= '2026-12-30T00:00:00Z'
  AND (valid_to IS NULL OR valid_to > '2026-12-30T00:00:00Z')
ORDER BY version DESC
LIMIT 1

This is not a query the developer writes. The FLIN compiler generates it from the @ operator syntax. The developer writes user @ yesterday. The compiler does the rest.

The Architecture: Six Layers Deep

Making temporal access feel like a language primitive required changes at every layer of the FLIN compiler and runtime. This was not a feature we could bolt on at the end -- it had to be woven into the fabric of the language from lexer to database.

Layer 1: Lexer. The @ token was added to the lexer alongside seven temporal keywords: now, today, yesterday, tomorrow, last_week, last_month, last_year. These are first-class tokens, not string literals or library functions.

Layer 2: Parser. The parser recognizes entity @ temporal_reference and constructs an Expr::Temporal AST node. The temporal reference can be a negative integer (relative version), a date string (absolute time), or a keyword.

Layer 3: Type Checker. The type checker validates that the left side of @ is an entity type and the right side is an integer, time, or text (for date strings). It returns an optional entity type because the requested version might not exist.

// Type checker validation for @ expressions
match &time_ty {
    FlinType::Int | FlinType::Time | FlinType::Text => {}
    _ => return Err(TypeError::with_hint(
        format!("Expected time reference, found {}", time_ty),
        span,
        "Use a version number like -1, a time keyword like yesterday, \
         or a date string like \"2024-01-15\"",
    )),
}

Layer 4: Code Generator. The code generator emits different bytecode depending on the type of temporal reference. Relative version access emits OpCode::AtVersion. Time keywords emit OpCode::AtTime with a time code byte. Date strings emit OpCode::AtDate.

Layer 5: Virtual Machine. The VM handles each temporal opcode by looking up the entity's version history and finding the matching version. For relative access, it indexes directly. For time-based access, it performs a binary search on the timestamp-ordered version list.

Layer 6: Database (ZeroCore). The storage layer maintains a version history map keyed by (entity_type, entity_id). Each entry is a vector of EntityVersion structs ordered by timestamp. The get_history() method returns the complete timeline; the find_at_version() method returns a specific snapshot.

/// Historical version of an entity for time-travel queries
#[derive(Debug, Clone)]
pub struct EntityVersion {
    pub version: u64,
    pub timestamp: i64,
    pub fields: HashMap<String, Value>,
}

Why Not Event Sourcing?

The obvious comparison is event sourcing, and we considered it. In event sourcing, you store a sequence of events (UserNameChanged, UserEmailUpdated) and reconstruct state by replaying them. FLIN's temporal model is different: we store complete snapshots at each version.

The trade-off is deliberate. Event sourcing is powerful for systems where the events themselves are the primary domain model -- financial transactions, order workflows, collaborative editing. But for the majority of applications, developers do not think in events. They think in states: "What was this user's name last Tuesday?" Answering that question in an event-sourced system requires replaying every event up to that point. In FLIN, it is a single indexed lookup.

Snapshot-based versioning also simplifies the programming model. There is no concept of "projections" or "event handlers" or "saga orchestrators." You save an entity. You query a past version. That is it.

The storage cost is higher -- each version stores a complete copy of all fields, not just the delta. We address this with planned retention policies that compact old versions, and in practice, the convenience of instant point-in-time queries far outweighs the extra storage for the applications FLIN targets.

Version Metadata: Not Just Data, But Context

Each version carries metadata that the developer can access without any special API:

user = User.find(id)
print(user.version_number)    // 5
print(user.created_at)        // 2026-12-31T14:30:00Z
print(user.updated_at)        // 2026-12-31T18:45:00Z

This metadata is automatically populated by the runtime. When you iterate over an entity's history, each version in the list has its own metadata:

{for ver in product.history}
    <div class="audit-entry">
        <p>Version #{ver.version_number}</p>
        <p>Created: {ver.created_at}</p>
        <p>Price: ${ver.price}</p>
    </div>
{/for}

Building an audit trail -- the feature that used to require a dedicated table, triggers, and a separate UI -- is now a {for} loop.

The Immutability Guarantee

One design decision we debated at length was immutability. Could you modify a past version? Our answer was an unconditional no.

old_user = user @ -1

// This affects the CURRENT version only user.name = "New Name" save user

// Old version unchanged print((user @ -2).name) // Still "Juste" ```

Allowing mutation of historical versions would undermine every use case the temporal model enables. Audit trails become meaningless if someone can rewrite history. Compliance requirements demand immutable records. Debugging becomes impossible if past states can shift under your feet.

The only way to "change" history is through the destroy keyword, which permanently removes an entity and all its versions -- a deliberate, administrative action equivalent to GDPR's "right to be forgotten." We cover that in detail in Article 049.

Real-World Use Cases

The temporal model is not an academic exercise. It enables concrete product features with minimal code.

Price history. E-commerce applications can show price evolution without a separate price history table:

entity Product {
    name: text
    price: money
}

{for version in product.history.last(10)}
{version.created_at.format("MMM DD")} {version.price}
{/for}
```

Undo functionality. Any entity can be reverted to its previous state:

fn undo(entity) {
    previous = entity @ -1
    if previous {
        entity.name = previous.name
        entity.status = previous.status
        save entity
    }
}

Compliance and audit. Every field change is recorded with a timestamp, satisfying regulatory requirements without additional infrastructure.

Data recovery. Accidental changes are always recoverable because the previous version still exists.

Performance Considerations

The temporal model adds overhead -- there is no pretending otherwise. Every save operation writes two records instead of one (the new version and the history entry). Every entity consumes more storage as versions accumulate.

We mitigated this with several design choices:

OperationComplexityNotes
Current versionO(1)Indexed by id + valid_to=NULL
Specific versionO(log n)Indexed by id + version
At timeO(log n)Indexed by id + valid_from
Full historyO(n)Returns all versions

Current version access -- the operation that happens on every page load -- is O(1). You only pay the cost of history when you actually query it. And for the planned @retention annotation, developers will be able to set per-entity retention policies:

entity Metric {
    value: number

@retention(90.days) // Keep 90 days of history } ```

What We Built in Session 012

The initial temporal infrastructure was built in Session 012, a fifty-minute sprint that added the EntityVersion struct, the version history map in the VM, temporal query opcodes, and version history pruning methods. Ten new tests. Three hundred and one total tests passing.

That session established the skeleton. The next twenty sessions would add muscle: fixing the AtTime stub so keywords actually worked, implementing the .history property, reaching one hundred percent test coverage, and adding the full suite of temporal comparison helpers.

The temporal model was the single most ambitious feature in FLIN's design. It touched every layer of the compiler. It required rethinking how data flows through the entire system. And it delivered on a promise that no other language we know of makes: every entity remembers everything, automatically, forever.

---

This is Part 1 of the "How We Built FLIN" temporal model series, documenting the design and implementation of FLIN's memory-native data system.

Series Navigation: - [046] Every Entity Remembers Everything: The Temporal Model (you are here) - [047] Version History and Time Travel Queries - [048] Temporal Integration: From Bugs to 100% Test Coverage - [049] Destroy and Restore: Soft Deletes Done Right - [050] Temporal Filtering and Ordering - [051] Temporal Comparison Helpers - [052] Version Metadata Access - [053] Time Arithmetic: Adding Days, Comparing Dates - [054] Tracking Accuracy and Validation - [055] The Temporal Model Complete: What No Other Language Has

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles