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.