Event Log
Table of contents
A temporal primitive: an append-only sequence of immutable, time-ordered events. The substrate every audit, history, replay, and event-sourcing pattern composes on top of.
Intent
A composing pattern records facts about state changes. The Event Log preserves those facts as an append-only sequence — each event immutable once recorded, ordered by append, queryable but never editable.
The pattern addresses a class of needs that recur across virtually every system that mutates state: audit trails, undo histories, activity feeds, event sourcing, write-ahead logs, replication journals, version-control logs, replay buffers. The shape is constant — a stream of facts, recorded in order, never altered after the fact, available for retrospective query.
This is a freestanding (can be specified without naming any other pattern) concept in the EOS (Essence of Software — Daniel Jackson’s framework for specifying software concepts as freestanding, composable units) sense. It carries its own state (the sequence), its own actions (append, read), and its own operational principles (append-only — records can be added but never changed or deleted, total order, immutability — unchangeable once written). Composing patterns wrap it with retention policies, tamper-evidence, actor identity, reverse-lookup indices, and so on. The Event Log itself imposes no semantics on what an event means; it imposes only the structural guarantee that the sequence is faithful to what was recorded.
Summary
Event Log is a foundational temporal atom (a freestanding pattern spec — one that can be specified without naming any other pattern) that provides a single, simple guarantee: anything appended to the log stays in the log, in the order it arrived, forever (within the lifetime of the log instance), unchanged. It is the substrate from which audit trails, undo histories, activity feeds, event-sourced systems, replication journals, and write-ahead logs are all built. The pattern exposes exactly two actions: append, which adds a new event at the tail and returns an opaque identifier (a system-generated ID with no meaningful content) for that event; and read, which returns events in strict sequence order for any well-formed query. There is no edit and no delete surface — the log is append-only (records can be added but never changed or deleted) by design.
Each event carries four fields: its unique identifier, a strictly increasing sequence number that determines total order (meaning every pair of events has a defined earlier/later relationship with no ties, regardless of clock behavior), a wall-time (clock time as a human would read it, not an internal counter) timestamp for human-readable annotation, and an opaque payload that the Event Log does not interpret. The distinction between sequence number and wall-time is intentional and load-bearing: wall-time is best-effort and can be non-monotonic under clock skew, but sequence number is the authoritative ordering source and is always strictly monotonic. This means the log can be replayed faithfully even on systems with imperfect clocks.
The atom deliberately carries no opinions about retention duration, cryptographic integrity proof, actor attribution, payload schema, or indexing by payload field — all of those are separate composing patterns (Retention Window, Tamper Evidence, Actor Identity, Schema Evolution, Reverse Index respectively). This separation means the same Event Log atom underlies a personal task history, a healthcare clinical record, a bank transaction journal, and a regulated compliance audit trail, with the compliance-layer concerns layered on only where they are needed. Grounded (passed all required review passes and is stable enough to generate from) after one full three-pass review and one refinement round.
Structure
Identity model
Each event recorded in an Event Log has an opaque, immutable, system-generated event_id — assigned on append, never reused, never reassigned. The id is the event’s identity; data is a property of the event, not its identity.
Each Event Log is itself a named instance. Multiple instances coexist in real systems (one per audited subsystem, one per user history, one per replication stream). The atom specifies what one instance is and how it behaves; composing patterns decide how many instances to instantiate and with what configuration.
Inputs
- A sequence of
appendcalls from composing patterns. - A
readAPI for retrospective query. - An implicit clock providing wall-time (clock time as a human would read it, not an internal counter) timestamps.
Actions
append(data) → event_id | rejected(invalid-payload | storage-failure)— record a new event at the tail.read(query) → ordered_sequence_of_events | rejected(invalid-query)— return events matching the query, ordered bysequence_numberascending.
A query may name a sequence-number range, a wall-time range, a payload predicate, or a combination. The exact query shape is implementation policy; the atom requires only that any valid query returns events in sequence_number order.
Outputs
- For
append: a freshevent_id, or a rejection naming the failed precondition. - For
read: a (possibly empty) ordered sequence of events. Each event carries itsevent_id,sequence_number,recorded_at, anddata.
State
The log is a totally ordered sequence of events. Each event has:
event_id— opaque, immutable, unique within the log.sequence_number— strictly increasing integer assigned at append. Determines total order.recorded_at— wall-time when the event was appended. Annotates time but is not the source of total order.data— opaque payload supplied by the composing pattern. The Event Log does not interpret it.
The log itself has:
name— identifies the log instance among co-existing logs.next_sequence_number— the sequence number that the next appended event will receive. Begins at 1 for a fresh log instance and increments by 1 on each append. Part of the log instance’s persistent state — durable implementations must preserve it across restarts to maintain sequence-number monotonicity. Volatile implementations that reset to 1 on restart violate this invariant across the lifetime of the log instance.
There is no delete or edit surface. Once recorded, events remain; the log only grows.
Flow
The Event Log has no user-driven flow of its own; it is invoked by composing patterns.
- Composing pattern observes a state change. It calls
append(data)with a payload describing what happened. - Event Log records the event. Assigns
event_id,sequence_number = next_sequence_number,recorded_at = now. Incrementsnext_sequence_number. Returnsevent_id. - Time passes; more appends happen. Each receives a fresh, strictly larger
sequence_number. - A composing pattern queries the log. Calls
read(query). Receives an ordered sequence of matching events.
Decision points
- At
append(data)—datamust satisfy the configured payload constraints (default: max 64 KB, opaque bytes; configurable per instance). Otherwise rejected asinvalid-payload. There are no other preconditions; appends never fail for ordering or contention reasons — the underlying implementation must serialize them. If the store write fails after all preconditions are satisfied, the atom returnsrejected(storage-failure). Theevent_idis not returned; the caller must treatstorage-failureas definitive — the event did not land. A sequence number may have been allocated and consumed; see Edge cases. - At
read(query)—queryparameters must be well-formed (sequence-number range valid, time range valid, predicate parseable). Otherwise rejected asinvalid-query. A well-formed query that matches no events returns an empty sequence, not a rejection.
Behavior
How the concept appears to composing patterns:
- Append is durable on success. Once the caller receives an
event_id, the event is in the log and will appear in subsequent reads. - Reads are repeatable and monotonic. Reading the same query at two different times returns at least the events from the earlier read, plus any events appended in between. The log only grows.
- Order is total. Any two distinct events have a defined relative position via
sequence_number. Ties never occur, even for events appended in the same wall-time instant. - Wall-time is best-effort.
recorded_atis non-decreasing under a well-behaved clock. Under an unreliable or adversarial clock,recorded_atmay not be monotonic;sequence_numberremains the source of truth for ordering. - The log is unbounded by this atom alone. Retention, archival, and compaction belong to composing patterns; the bare Event Log keeps everything for the lifetime of the log instance.
Feedback
- After
append(data)— a new event exists in the log with a freshevent_id,sequence_number = previous_next,recorded_at = now.next_sequence_numberincrements by 1. The event is immediately visible to subsequent reads. - After
read(query)— a sequence of matching events in ascendingsequence_numberorder. The state of the log is unchanged.
Each rejected action produces an observable refusal naming the failed precondition (invalid-payload, invalid-query, or storage-failure).
Invariants
- Invariant 1 — Append-only. Once an event is in the log, it remains in the log for the lifetime of the log instance. No action removes events.
- Invariant 2 — Event immutability. After a successful
append, the event’sevent_id,sequence_number,recorded_at, anddatanever change. - Invariant 3 — Total order. For any two distinct events
e1ande2, exactly one ofe1.sequence_number < e2.sequence_numberore1.sequence_number > e2.sequence_numberholds. - Invariant 4 — Sequence-number monotonicity. If
e1was appended beforee2, thene1.sequence_number < e2.sequence_number. - Invariant 5 — Read consistency. A
readissued at timetreturns every event withsequence_number ≤ next_sequence_number − 1whose data matches the query, ordered bysequence_numberascending. - Invariant 6 — No id reuse. No two events in the log share an
event_id. - Invariant 7 — Wall-time best-effort monotonicity. Under a non-decreasing clock,
recorded_atis non-decreasing in append order. Under an unreliable clock, this is best-effort andsequence_numberis the authoritative order.
Append-only and event immutability together give the immutable journal property — the property that distinguishes an Event Log from a mutable record set. Total order and sequence-number monotonicity give the replay property. Read consistency gives the durable visibility property. No id reuse prevents identity collisions across time.
Examples
The same pattern, four domains, identical mechanic.
Personal Todo activity log
A composing system wraps each Personal Todo action as an event: {type: "add", id: "t1", description: "buy milk"}, {type: "complete", id: "t1"}, {type: "delete", id: "t1"}. The log records them in order, never alters them. The user can later query the log to see what they did this week, restore deleted tasks (compose with Reverse Index + Restore — see Undo History), or reason about completion patterns. The Personal Todo pattern itself is unchanged; the log is a side stream the composing application maintains.
Compliance audit log
A regulated system records every state-changing action: {type: "patient_record_accessed", patient_id: "p123", actor: "dr_smith", reason: "treatment", at: "2026-05-07T14:32:11Z"}. The log is append-only by definition. The Audit Trail application composes this atom with Retention Window, Tamper Evidence, and Actor Identity to add policy-bounded retention, integrity proof, and verifiable attribution. The Event Log itself doesn’t know what compliance means; it preserves the sequence faithfully and lets compliance be layered on.
Patient medical record (clinical history)
Each clinical observation, prescription, lab result, and vital sign is appended as an event with structured data. The clinical record is an Event Log; the patient chart is a view over it (latest values per field). Mistakes are corrected by appending a correction event, never by editing the original — the record must show what was originally recorded and when it was corrected. ICD coding, billing extraction, and longitudinal analytics all read the same log.
Bank transaction journal
Every credit, debit, transfer, and adjustment is appended as an event in the journal. Account balances are derived by replaying the journal up to a point in time. Reversals are appended as new events (a refund event referencing the original charge), never as edits. The journal is the source of truth; the balance display is a projection. Reconciliation, fraud detection, and regulatory reporting all read the same log.
The mechanic is identical across all four. What differs: payload schema, query patterns, and the composing patterns that derive views (current todo list, audit report, current chart, current balance) from the underlying log.
Rejection paths
A single sequence exercising all three rejection reasons:
append(65_000_bytes_of_data)→ rejectedinvalid-payload(payload exceeds the default 64 KB cap; configurable per instance).append({type: "deposit", amount: 500})→ accepted; returnsevent_id e1withsequence_number 1.read({sequence_range: [-1, 5]})→ rejectedinvalid-query(negative sequence number is not a well-formed range parameter).read({sequence_range: [1, 1]})→ returns[e1];sequence_number 1matches, ordered ascending.- Underlying store becomes temporarily unavailable.
append({type: "withdrawal", amount: 100})→ rejectedstorage-failure; event does not land; caller must treat the rejection as definitive. A sequence number may have been consumed; subsequent successful appends receive a strictly higher number, producing a gap in the dense sequence (see Edge cases — Sequence-number gaps on storage failure). - Store recovers.
append({type: "withdrawal", amount: 100})→ accepted; returnsevent_id e2with a sequence number strictly greater than 1.
All three rejection reasons (invalid-payload, invalid-query, storage-failure) exercised in one thread.
Edge cases and explicit non-goals
What this pattern does not cover:
- Retention and archival. The bare Event Log keeps everything forever. Compose with Retention Window for time-based pruning under regulatory obligation, and a Storage Tier pattern for active-versus-cold archival (orthogonal axis).
- Tamper-evidence. Events are immutable by spec, but nothing in the bare atom prevents an adversary with write access from rewriting the log. Cryptographic hash chains, signed events, and Merkle trees belong to a Tamper Evidence pattern that composes on top.
- Actor identity. The Event Log records what was appended; the composing pattern decides whether the payload includes a
who. Actor Identity standardizes that addition with a verifiable non-repudiation binding. - Reverse lookup / indexing. The Event Log supports forward iteration and queries by sequence-number or time range. Lookup by payload field (find all events of type X, find all events touching id Y) is the job of a separate Reverse Index pattern.
- Distributed consistency. A single Event Log instance is a single ordered sequence on one host. Multi-host ordering across instances (causal order, vector clocks, Lamport timestamps) belongs to a Consensus or Causal Ordering pattern.
- Event schemas and evolution. The data payload is opaque. Schema definition, validation, and migration belong to a Schema Evolution pattern.
- Compaction and snapshots. Some event-sourced systems collapse event sequences into snapshots. The bare Event Log does not; Snapshot is a composing pattern.
- Subscriptions / change feeds. A pull-only
readAPI. Push-based notification of new events belongs to an Observer or Change Feed pattern. - Multi-event atomicity. Each
appendis atomic. Multi-event transactions (“append A and B together or neither”) belong to a Transaction pattern. - Durability across crashes. The atom specifies in-memory semantics. Persistence across process restarts is a deployment concern; durable implementations must provide write-ahead logging or equivalent. Append-only and event immutability are best-effort across crashes unless the implementation supplies durability.
- Right-to-be-forgotten erasure. Where law mandates true deletion of recorded events (GDPR — EU General Data Protection Regulation — Article 17, certain healthcare contexts), the architectural answer append corrections, never edit history breaks down. A composing pattern (Erasure Tombstone, Cryptographic Shredding) must be designed alongside legal counsel.
- Sequence-number gaps on storage failure. If an implementation allocates a sequence number before attempting the write, a
storage-failureconsumes that sequence number. The next successful append receives a strictly higher sequence number, creating a gap in the sequence. Sequence-number monotonicity (Invariant 4) is not violated — the invariant holds over successfully appended events only — but consumers who assume a dense sequence may misinterpret the gap as missing events. Implementations that want to avoid gaps must allocate sequence numbers only after the write succeeds, or use a rollback mechanism that returns the allocated number to the pool on write failure.
Where the pattern breaks down: when the host environment cannot supply atomic, serialized appends (most adversarially-distributed settings); when events must be edited or deleted in place; when ordering must be derived from something other than append order.
Composition notes
Patterns compose with Event Log through one of two contracts, often both:
- Append on every state change. The composing pattern emits an event to the log on every state transition. Personal Todo’s
add/edit/complete/deletewould each produce events. The Event Log is the durable record from which the composing pattern’s history can be reconstructed. - Replay to derive state. The composing pattern derives its current state by reading the log from the beginning (or from the most recent snapshot). This is the event-sourcing style — the log is the source of truth, current state is a projection.
Forthcoming compositions in compositions/:
- Undo History — Event Log + Reverse Index + Restore action.
- Audit Trail — Event Log + Actor Identity + Retention Window + Tamper Evidence. The canonical regulated-audit primitive; landed.
- Activity Feed — Event Log + Subscriber pattern + Filter.
- Event-Sourced Reservation — Event Log + Snapshot + Reservation atom.
In all four, Event Log is the substrate; the application adds the policy.
Standards references
Event Log is a foundational primitive with deep standards backing:
- ISO/IEC 27001 — information security management; mandates event logging for security-relevant actions.
- NIST SP 800-92 — Guide to Computer Security Log Management; describes log lifecycle, integrity, retention requirements.
- W3C Activity Streams 2.0 — JSON format for activity feeds; treats activities as events with actor / verb / object structure.
- Event Sourcing literature — Greg Young’s early write-ups; Martin Fowler’s Event Sourcing; foundational pattern in domain-driven design.
- Database write-ahead logging (WAL) — the same primitive at the storage layer; ARIES recovery, PostgreSQL WAL, MySQL binlog.
- Distributed-systems replication logs — Kafka topics, Raft logs, Paxos value logs.
- Version control — Git’s commit log is an Event Log with cryptographic tamper-evidence (Merkle DAG) layered on top.
It inherits from:
- Daniel Jackson, The Essence of Software — the conception of a freestanding concept with state, actions, and operational principles.
- Eiffel’s design-by-contract — preconditions on
appendandread. - Linear temporal logic (a formal notation for reasoning about sequences of states over time) — append-only, event immutability, and sequence-number monotonicity expressed as temporal properties (
always,until).
Status
grounded — 2026-05-20 — concept is freestanding, composable, has a verifiable invariant set, and four cross-domain examples spanning productivity, compliance, healthcare, and finance. Ready for composition with Undo History, Audit Trail, Activity Feed, and event-sourced applications.
Lineage notes
This pattern survived all three pressure-testing passes (see PRESSURE_TESTING.md) on its first revision.
Pass 1 — Structural completeness (GRID). Clean. All nine nodes addressed; the Edge cases section enumerates eleven explicit out-of-scope concerns, each pointing at a composing pattern that handles it (Retention Window, Tamper Evidence, Actor Identity, Reverse Index, Consensus, Schema Evolution, Snapshot, Observer, Transaction, durability, Erasure Tombstone).
Pass 2 — Conceptual independence (EOS). Clean. Event Log is itself a foundational primitive that other concepts compose on top of. The concerns most often candidates for extraction (retention, tamper-evidence, actor identity, indexing) are already correctly named as composing patterns rather than absorbed into the atom.
Pass 3 — Adversarial scrutiny (Linus mode). Three findings, one fixed in-pattern, two already adequately addressed:
next_sequence_numberbehavior across restarts. The first draft said “begins at 1 and increments by 1 on each append” without addressing what happens if the log instance is durable and survives a restart. Fixed: the State section now specifies the counter is part of the log instance’s persistent state and that durable implementations must preserve it across restarts to maintain sequence-number monotonicity.- Query DSL ambiguity. Already named explicitly as implementation policy. The atom guarantees only that any well-formed query returns events in
sequence_numberorder; the predicate language is intentionally deferred to composing patterns and implementations. - Append/read concurrency. Already addressed under Decision points (appends serialized by the underlying implementation) and under durability in Edge cases.
The pattern is grounded — 2026-05-13 after one round.
Refinement round 1. Five findings, all closed in-pattern. Conventions inherited from the methodology directly.
appendsignature usedrejected(reason)placeholder. The actual reason wasinvalid-payload;storage-failurewas absent entirely. Resolved: signature expanded torejected(invalid-payload | storage-failure). Decision points updated — if the store write fails after all preconditions pass, the atom returnsrejected(storage-failure); theevent_idis not returned; the caller must treat the rejection as definitive. The Behavior section’s “append is durable on success” guarantee has a converse: if the caller receivesstorage-failure, the event did not land.readsignature omitted its rejection form entirely. The signature showed onlyordered_sequence_of_events; Decision points and Feedback both namedinvalid-queryas a rejection but it was absent from the signature. Resolved: signature updated toread(query) → ordered_sequence_of_events | rejected(invalid-query).- Feedback section missing
storage-failure. The enumeration of rejection reasons listedinvalid-payloadandinvalid-queryonly. Resolved:storage-failureadded to the enumeration. - Storage-failure not addressed in Decision points. Resolved:
appendDecision point extended — storage-failure path, the consequence (event did not land), and the sequence-number allocation note added. - Sequence-number gap on storage failure not addressed. Implementations that allocate a sequence number before the write attempt may consume that number on
storage-failure, creating a gap in the sequence. Invariant 4 is not violated (it holds over successfully appended events only), but consumers expecting a dense sequence may misinterpret the gap. Resolved: new Edge case — Sequence-number gaps on storage failure — added with guidance for gap-free implementations.
Scheduled rescan: 2026-05-20. Pass 1 clean. Pass 2 clean. Pass 3 — one refining finding: examples covered only happy-path append sequences across four domains; no example exercised the rejection paths (invalid-payload, invalid-query, storage-failure). All three rejection reasons were named in Decision points and Feedback but not demonstrated with concrete values. Resolved: fifth example — Rejection paths — added, walking all three rejection reasons in a single thread including the sequence-number gap consequence of a storage-failure. Round closes clean.