129 Event Sourcing

In an event-source system entity types are event types. For example:


namespace Genealogy
{
    enum Month
    {
        jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
    }
    
    // By default order is lexicographic on the order in which the base attributes appear.
    value Date
    {
        Integer year;
        Month month;
        Integer day;
    }

    value Time
    {
        Integer hour where hour ∈ [0..23];
        Integer minute where minute ∈ [0..59];
        Integer second where second ∈ [0..59];
    }

    /*
    value Date
    {
        Integer year;
        Month month;
        Integer day;

        // Lexicographic order on year descending then month ascending then day ascending
        order [year ↓, month, day];
    }

    // Another way to define an order
    Boolean <(Date d1, Date d2) =
        d1.year < d2.year
        ∨ (d1.year = d2.year ∧ d1.month < d2.month)
        ∨ (d1.year = d2.year ∧ d1.month = d2.month ∧ d1.day < d2.day);
    */

    value DateTime
    {
        Date date;
        Time time;
    }
    
    /*
    In a true events database, the GenealogySystem would just record events in base attributes.  
    Person person* would be a derived attribute!

    entity GenealogySystem
    {
        BirthEvent birth*;
        DeathEvent death*;
        MarriageEvent marriage*;
        DivorceEvent divorce*;

        → Person person* =
            birth.child
            | birth.mother
            | birth.father
            | death.person
            | marriage.spouse
            | divorce.spouse;
    }
    */

    entity GenealogySystem
    {
        Person person*;
        → MarriageEvent marriage* = person.marriage;
        → DivorceEvent divorce* = person.divorce;
        → birth* = (person,person.birth);
        → death* = (person,person.death);
    }

    enum Gender
    {
        male,
        female
    }

    entity Person
    {
        Integer id;
        String name;
        embed Gender;
        BirthEvent birth?;
        DeathEvent death?;
        MarriageEvent marriage* where let p=this in marriage.(p ∈ spouse.Σ);
        DivorceEvent divorce* where let p=this in divorce.(p ∈ spouse.Σ);

        → Person mother?            = birth.mother;
        → Person father?            = birth.father;
        → Person parent*            = mother | father;

        Person child* where let p=this in child.(p ∈ parent.Σ);

        → Person grandparent*       = parent.parent;
        → Person greatgrandparent*  = grandparent.parent;
        → Person grandchild*        = child.child;
        → Person greatgrandchild*   = grandchild.child;
        → Person son*               = child.[male];
        → Person daughter*          = child.[female];
        → Person sibling*           = let p=this in parent.child.[this ≠ p];
        → Person brother*           = sibling.[male];
        → Person sister*            = sibling.[female];
        → Person cousin*            = parent.sibling.child;
        → Person cousinOnceRemoved* = cousin.child | parent.cousin;
        → Person secondCousin*      = parent.cousin.child;
        → Person ancestor*          = parent | parent.ancestor;
        → Person descendant*        = child | child.descendant;
        → Person auntOrUncle*       = parent.sibling;
        → Person aunt*              = auntOrUncle.[female];
        → Person uncle*             = auntOrUncle.[male];
        → Person nieceOrNephew*     = sibling.child;
        → Person niece*             = nieceOrNephew.[female];
        → Person nephew*            = nieceOrNephew.[male];
        → Person parentInLaw*       = spouse.parent;
        → Person siblingInLaw*      = spouse.sibling | sibling.spouse;
        → Person childInLaw*        = child.spouse;

        // Doesn't account for remarry
        //→ Person spouse*            = marriage.spouse ∖ (this | divorce.spouse);

        → RelationshipEvent? lastRelationshipEvent = (marriage | divorce).orderBy(datetime).last;
        
        // Assuming monogamy
        → Person spouse? =
            if lastRelationshipEvent is MarriageEvent
                then lastRelationshipEvent.spouse ∖ this
            else none;

        → Boolean married = (spouse ≠ none);

        @predicate("The person's current age is [age]")
        → Integer age? =
            if birth ≠ none ∧ death ≠ none then
                yearsBetween(death.datetime, birth.datetime)
            else if birth ≠ none then
                yearsBetween(now, birth.datetime)
            else
                none;

        /*
        @predicate("The person is currently married")
        → Boolean married =
            (marriage | divorce)
                .orderBy(datetime)
                .Σ{ false ;
                    marriedSoFar  evt ->
                        if evt is MarriageEvent then true
                        else if evt is DivorceEvent then false
                        else marriedSoFar 
                    };
        */

        key {id};

        constraints 
        { 
            // ∀M:Person (this.mother(M) → M ≠ this)
            mother ≠ this;

            // ∀F:Person (this.father(F) → F ≠ this)
            father ≠ this;

            // ∀M:Boolean ∀A:Integer (this.married(M) ∧ this.age(A) ∧ M → A ≥ 18)
            married → age ≥ 18;
        }
    }

    entity BirthEvent
    {
        DateTime datetime;
        String location?;
        Person mother?;
        Person father?;

        constraints
        {
            mother.female;
            father.male;
        }
    }

    entity DeathEvent
    {
        DateTime datetime;
        String location?;
    }

    abstract entity RelationshipEvent
    {
        Person spouse[2];
        DateTime datetime;

        // This key constraint on an abstract entity matters if there is an attribute of type 
        // RelationshipEvent that allows for cardinality greater than 1.   
        // It matters for intermediate expressions too, such as (marriage | divorce)
        key {spouse, datetime};
    }

    entity MarriageEvent isa RelationshipEvent
    {
        String location?;
    }

    entity DivorceEvent isa RelationshipEvent
    {
    }
}

Derived attributes are available in queries

Derived attributes provide logical independence by allowing queries to be written without regard for the distinction between base and derived attributes.

As an example, p.(name, sibling) is equivalent to p.(name, parent.child ∖ this as sibling).

p.(sibling, cousin, ancestor, niece) is equivalent to p.(parent.child ∖ this as sibling, parent.sibling.child as cousin, (parent | parent.ancestor) as ancestor, sibling.child.[gender=female] as niece)

Pushing birth, death, marriage and divorce events under a person domain

A useful principle when developing a Predica schema is to push information down into the domain where it naturally belongs. This tends to produce a more modular and simpler design. When an entity type is defined over a smaller, more specific domain, it no longer needs an explicit identifier. The surrounding context provides identity implicitly. For example, in this schema a BirthEvent does not need to identify the person who was born, because the event is stored under that person.

Birth and death events are therefore pushed down into the domains where they belong. A birth event belongs to the person being born, and likewise a death event belongs to the person who died.

Global populations become derived, not base

Pushing marriages and divorces down into Person eliminates the need for using inverse to derive inverse relationships. This is a case where it is better to derive global populations (which involves navigation downwards) than to make them base and require subdomains to navigate upwards.

This fits the Predica principle of pushing information downward to where it belongs.

As a result, many global populations become derived, not base. For example, the global marriage* and divorce* populations are defined by downward navigation: start at the system, go down to all persons, and collect their marriage or divorce events. This is cheap, clean, and compositional.


entity GenealogySystem
{
    Person person*;
    → MarriageEvent marriage* = person.marriage;  // downward navigation
    → DivorceEvent divorce* = person.divorce;     // downward navigation
}

This flips RM modelling

Predica’s philosophy is: Information belongs where it is most specific. Derived information belongs where it is most general.

This genealogy schema is a perfect example:

  • Base information (specific): A marriage event belongs to the two people involved. Therefore shared marriage events are stored under Person.
  • Derived information (general): The global set of all marriages belongs to the system. So they are derived from Person.

This is the opposite of conventional RM-style modelling, where global relvars are base and relationships are reconstructed upward. Predica flips that on its head. The genealogy schema demonstrates why that’s better.

Sharing a single marriage event between two spouses

This schema achieves full, symmetric sharing of each marriage event between its two spouses without needing a base relation of all marriage events. Person.marriage* is the base representation, and GenealogySystem.marriage* is a derived population.

The key {spouse, date} of MarriageEvent canonicalises identity. This is the crucial mechanism. If Alice and Bob both store a marriage event with the same spouse pair and date, then they refer to the same logical MarriageEvent. If only one spouse stores it, then the constraints force the other spouse to store it too. This achieves shared identity without a global base relation.

The compiler can implement this with a single shared object because the key canonicalises identity, the event appears under both spouses, the global population is derived, and there is no duplication. Therefore, the compiler is free to implement one MarriageEvent object referenced by both Person objects, with no global base relation, and no need for reconciliation logic This is a very efficient object-centric representation.

Performance advantages of pushing information downwards

Roughly speaking, encoded data in memory or on disk should be read or written in chunks whose size is on the order of the product of latency and bandwidth for the device. This product can be fairly large - e.g. 64 kB. In such cases it is advantageous to localise related information. In a genealogy database it is better to localise all information about a person so it can be accessed as a single chunk.

Folding over events

When folding over historical events, the fold must operate on a list ordered by event time. Assignment-like updates are not commutative, so unordered folds over sets are not appropriate. Predica expresses this by combining .orderBy(date) with a fold.

Derived attributes

Predica’s derived attributes are not “computed columns” or “views”. They are semantic operators that extend the type itself. They let you build a vocabulary of domain concepts directly into the model, so that:

  • queries become simpler
  • constraints become clearer
  • reasoning becomes easier
  • the model becomes more expressive

It is particular useful for event sourcing because derived attributes can hold the current state as folds over history.

Folds and Event Sourcing

Event-sourced systems represent the state of an entity not by storing its current attributes directly, but by storing the events that describe how the entity changed over time. The current state is obtained by folding over those events.

Predica’s general fold operator is a natural fit for this pattern. An event stream is simply a set of events, and a fold reduces that set to a single value by accumulating the effect of each event. Because folds are associative and order-insensitive, they work cleanly with Predica’s set-based semantics.

Reconstructing state from events

Suppose a person’s life events include birth, marriage, divorce, and death. We can reconstruct derived attributes by folding over the relevant events. When folding over history it is generally necessary to use lists ordered by date. This is because assignment is not commutative.

For example, to compute whether a person is currently married:


person.(marriages ∪ divorces)
      .orderBy(date)
      .Σ{ false ;
          married evt ->
              if evt is MarriageEvent then true
              else if evt is DivorceEvent then false
              else married
        }

This fold walks through all marriage-related events and accumulates the current marital status. The initial value is false, and each event updates the accumulated value.

Computing age from birth and death events

A person’s age can be reconstructed by folding over their birth and death events:


person.(birth, death)
    .Σ{ none ;
        age (b, d) ->
            if b exists and d exists then d.date - b.date
            else if b exists then today - b.date
            else none
      }

The fold combines the birth and death information into a single derived value.

Accumulating children, spouses, and relationships

Event sourcing often requires reconstructing collections, not just scalars. Folds make this straightforward:


// All children of a person
person.births.Σ{ {} ; a e -> a ∪ { e.child } }

// All spouses (past and present)
(person.marriages ∪ person.divorces)
    .Σ{ {} ;
        a e ->
            if e is MarriageEvent then a ∪ { e.spouse1, e.spouse2 }
            else a
      }

These folds accumulate sets of related entities by walking the event history.

Why folds are ideal for event sourcing

  • Uniformity: Events are just values in a set; folds reduce them.
  • No special syntax: Event reconstruction uses the same fold mechanism as aggregates.
  • Declarative: The fold describes what the accumulated state is, not how to iterate.
  • Composable: Folds can be nested, combined, and reused across domains.
  • Purely functional: No mutation, no ordering assumptions, no procedural logic.

In this way, Predica treats event sourcing as a natural application of its general reduction mechanism. The same fold operator that defines Σsum and Σcount also reconstructs the state of an entity from its event history.