{"id":37208029,"url":"https://github.com/deluxeowl/chronicle","last_synced_at":"2026-01-14T23:54:40.406Z","repository":{"id":310992694,"uuid":"986246126","full_name":"DeluxeOwl/chronicle","owner":"DeluxeOwl","description":"Pragmatic, type safe event sourcing framework for Go.","archived":false,"fork":false,"pushed_at":"2025-11-29T12:46:23.000Z","size":1141,"stargazers_count":143,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-12-01T14:34:38.019Z","etag":null,"topics":["cqrs","cqrs-es","ddd","domain-driven-design","event-driven","event-sourcing","go","golang","golang-library"],"latest_commit_sha":null,"homepage":"https://pkg.go.dev/github.com/DeluxeOwl/chronicle","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DeluxeOwl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-05-19T10:21:04.000Z","updated_at":"2025-12-01T13:47:10.000Z","dependencies_parsed_at":"2025-08-21T13:37:35.284Z","dependency_job_id":"3601ca78-eaf9-4452-bdd3-f7f1d2221c35","html_url":"https://github.com/DeluxeOwl/chronicle","commit_stats":null,"previous_names":["deluxeowl/chronicle"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/DeluxeOwl/chronicle","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeluxeOwl%2Fchronicle","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeluxeOwl%2Fchronicle/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeluxeOwl%2Fchronicle/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeluxeOwl%2Fchronicle/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DeluxeOwl","download_url":"https://codeload.github.com/DeluxeOwl/chronicle/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeluxeOwl%2Fchronicle/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28439584,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T22:37:52.437Z","status":"ssl_error","status_checked_at":"2026-01-14T22:37:31.496Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cqrs","cqrs-es","ddd","domain-driven-design","event-driven","event-sourcing","go","golang","golang-library"],"created_at":"2026-01-14T23:54:39.679Z","updated_at":"2026-01-14T23:54:40.394Z","avatar_url":"https://github.com/DeluxeOwl.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n    \u003cimg alt=\"chronicle\" src=\"assets/logov2.png\" width=\"128\" height=\"128\"\u003e\n    \u003ch1\u003eChronicle\u003c/h1\u003e\n\u003c/div\u003e\n\n\u003cdiv align=\"center\"\u003e\n  \u003ch3 align=\"center\"\u003eA pragmatic and type-safe toolkit for \u003cbr/\u003emodern event sourcing in Go.\u003c/h3\u003e\n  \u003ca href=\"mailto:andreisurugiu.tm@gmail.com\"\u003e\u003ci\u003eWant to hire me?\u003c/i\u003e\u003c/a\u003e\n\u003c/div\u003e\n\n- [Quickstart](#quickstart)\n- [What is event sourcing?](#what-is-event-sourcing)\n- [Why event sourcing?](#why-event-sourcing)\n- [Why not event sourcing?](#why-not-event-sourcing)\n- [Optimistic Concurrency \\\u0026 Conflict Errors](#optimistic-concurrency--conflict-errors)\n\t- [Handling conflict errors](#handling-conflict-errors)\n\t- [Retry with backoff](#retry-with-backoff)\n\t- [Custom retry](#custom-retry)\n\t- [How is this different from SQL transactions?](#how-is-this-different-from-sql-transactions)\n\t- [Will conflicts be a bottleneck?](#will-conflicts-be-a-bottleneck)\n- [Snapshots](#snapshots)\n\t- [Snapshot policies](#snapshot-policies)\n- [Shared event metadata](#shared-event-metadata)\n- [Ordering: Event log \\\u0026 Global event log](#ordering-event-log--global-event-log)\n- [Storage backends](#storage-backends)\n\t- [Event logs](#event-logs)\n\t- [Snapshot stores](#snapshot-stores)\n- [Event transformers](#event-transformers)\n\t- [Example: Crypto shedding for GDPR](#example-crypto-shedding-for-gdpr)\n\t- [Global Transformers with `AnyTransformerToTyped`](#global-transformers-with-anytransformertotyped)\n\t- [Event versioning and upcasting](#event-versioning-and-upcasting)\n\t\t- [Merging events for compaction](#merging-events-for-compaction)\n- [Projections](#projections)\n\t- [`event.TransactionalEventLog` and `aggregate.TransactionalRepository`](#eventtransactionaleventlog-and-aggregatetransactionalrepository)\n\t- [Example](#example)\n\t- [Example with outbox](#example-with-outbox)\n\t- [Synchronous Projections (`event.SyncProjection`)](#synchronous-projections-eventsyncprojection)\n\t\t- [Example: System Wide Constraints - Unique Usernames](#example-system-wide-constraints---unique-usernames)\n\t- [Asynchronous Projections (`event.AsyncProjection`)](#asynchronous-projections-eventasyncprojection)\n\t- [Types of projections](#types-of-projections)\n\t\t- [By Scope](#by-scope)\n\t\t- [By Behavior](#by-behavior)\n\t\t- [By Data Transformation](#by-data-transformation)\n\t\t- [By Consistency Guarantees](#by-consistency-guarantees)\n\t\t- [By How Often You Update](#by-how-often-you-update)\n\t\t- [By Mechanism of Updating](#by-mechanism-of-updating)\n- [Event deletion](#event-deletion)\n\t- [Event archival](#event-archival)\n- [Implementing a custom `event.Log`](#implementing-a-custom-eventlog)\n\t- [The `event.Reader` interface](#the-eventreader-interface)\n\t- [The `event.Appender` interface](#the-eventappender-interface)\n\t- [Global Event Log (the `event.GlobalReader` interface)](#global-event-log-the-eventglobalreader-interface)\n\t- [Transactional Event Log](#transactional-event-log)\n- [Implementing a custom `aggregate.Repository`](#implementing-a-custom-aggregaterepository)\n\t- [Using an `aggregate.FusedRepo`](#using-an-aggregatefusedrepo)\n- [Contributing](#contributing)\n\t- [Devbox](#devbox)\n\t- [Automation](#automation)\n\t- [Workflow](#workflow)\n- [How the codebase is structured](#how-the-codebase-is-structured)\n\t- [Core Packages](#core-packages)\n\t- [Pluggable Implementations](#pluggable-implementations)\n\t- [Package Dependencies](#package-dependencies)\n\t- [Testing](#testing)\n- [Benchmarks](#benchmarks)\n- [Acknowledgements](#acknowledgements)\n- [TODO](#todo)\n\n\n## Quickstart\n\n\u003e [!WARNING]\n\u003e I recommend going through the quickstart, since all examples use the `Account` struct used below from the `account` package.\n\nInstall the library\n```sh\ngo get github.com/DeluxeOwl/chronicle\n\n# for debugging\ngo get github.com/sanity-io/litter\n```\n\nDefine your aggregate and embed `aggregate.Base`. This embedded struct handles the versioning of the aggregate for you.\n\nWe'll use a classic yet very simplified bank account example:\n```go\npackage account\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/DeluxeOwl/chronicle/aggregate\"\n\t\"github.com/DeluxeOwl/chronicle/event\"\n)\n\ntype Account struct {\n\taggregate.Base\n}\n```\n\nDeclare a type for the aggregate's ID. This ID type **MUST** implement `fmt.Stringer`. You also need to add an `ID()` method to your aggregate that returns this ID.\n\n```go\ntype AccountID string\n\nfunc (a AccountID) String() string { return string(a) }\n\ntype Account struct {\n\taggregate.Base\n\n\tid AccountID\n}\n\nfunc (a *Account) ID() AccountID {\n\treturn a.id\n}\n```\n\nDeclare the event type for your aggregate using a sum type (we're also using the [go-check-sumtype](https://github.com/alecthomas/go-check-sumtype) linter that comes with [golangci-lint](https://golangci-lint.run/)) for type safety:\n```go\n//sumtype:decl\ntype AccountEvent interface {\n\tevent.Any\n\tisAccountEvent()\n}\n```\n\nNow declare the events that are relevant for your business domain.\n\nThe events **MUST** be side effect free (no i/o).\nThe event methods (`EventName`, `isAccountEvent`) **MUST** have pointer receivers:\n\n```go\n// We say an account is \"opened\", not \"created\"\ntype accountOpened struct {\n\tID         AccountID `json:\"id\"`\n\tOpenedAt   time.Time `json:\"openedAt\"`\n\tHolderName string    `json:\"holderName\"`\n}\n\nfunc (*accountOpened) EventName() string { return \"account/opened\" }\nfunc (*accountOpened) isAccountEvent()   {}\n```\n\nBy default, events are encoded to JSON (this can be changed when you configure the repository).\n\nTo satisfy the `event.Any` interface (embedded in `AccountEvent`), you must add an `EventName() string` method to each event.\n\nLet's implement two more events:\n\n```go\ntype moneyDeposited struct {\n\tAmount int `json:\"amount\"` // Note: In a real-world application, you would use a dedicated money type instead of an int to avoid precision issues.\n}\n\n// ⚠️ Note: the event name is unique\nfunc (*moneyDeposited) EventName() string { return \"account/money_deposited\" }\nfunc (*moneyDeposited) isAccountEvent()   {}\n\ntype moneyWithdrawn struct {\n\tAmount int `json:\"amount\"`\n}\n\n// ⚠️ Note: the event name is unique\nfunc (*moneyWithdrawn) EventName() string { return \"account/money_withdrawn\" }\nfunc (*moneyWithdrawn) isAccountEvent()   {}\n```\n\n\n\nYou must now \"bind\" these events to the aggregate by providing a constructor function for each one. This allows the library to correctly decode events from the event log back into their concrete types.\n\nYou need to make sure to create a constructor function for each event:\n\n```go\nfunc (a *Account) EventFuncs() event.FuncsFor[AccountEvent] {\n\treturn event.FuncsFor[AccountEvent]{\n\t\tfunc() AccountEvent { return new(accountOpened) },\n\t\tfunc() AccountEvent { return new(moneyDeposited) },\n\t\tfunc() AccountEvent { return new(moneyWithdrawn) },\n\t}\n}\n```\n\nLet's go back to the aggregate, and define the fields relevant to our business domain (these fields will be populated when we replay the events):\n```go\ntype Account struct {\n\taggregate.Base\n\n\tid AccountID\n\n\topenedAt   time.Time\n\tbalance    int // we need to know how much money an account has\n\tholderName string\n}\n```\n\nNow we need a way to build the aggregate's state from its history of events. This is done by \"replaying\" or \"applying\" the events to the aggregate.\nYou shouldn't check business logic rules here, you should just recompute the state of the aggregate.\n\nWe'll enforce business rules in commands. \n\nNote that the event structs themselves are unexported. All external interaction with the aggregate should be done via commands, which in turn generate and record events.\n\n```go\nfunc (a *Account) Apply(evt AccountEvent) error {\n\tswitch event := evt.(type) {\n\tcase *accountOpened:\n\t\ta.id = event.ID\n\t\ta.openedAt = event.OpenedAt\n\t\ta.holderName = event.HolderName\n\tcase *moneyWithdrawn:\n\t\ta.balance -= event.Amount\n\tcase *moneyDeposited:\n\t\ta.balance += event.Amount\n\tdefault:\n\t\treturn fmt.Errorf(\"unexpected event kind: %T\", event)\n\t}\n\treturn nil\n}\n```\n\nThis is type safe with the `gochecksumtype` linter.\n\nIf you didn't add any cases, you'd get a linter error:\n```\nexhaustiveness check failed for sum type \"AccountEvent\" (from account.go:24:6): missing cases for accountOpened, moneyDeposited, moneyWithdrawn (gochecksumtype)\n```\n\nNow, let's actually interact with the aggregate: what can we do with it? what are the **business operations** (commands)?\n\nWe can **open an account**, **deposit money** and **withdraw money**.\n\nLet's start with opening an account. This will be a \"factory function\" that creates and initializes our aggregate.\n\nFirst, we define a function that returns an empty aggregate, we'll need it later and in the constructor:\n```go\nfunc NewEmpty() *Account {\n\treturn new(Account)\n}\n```\n\nAnd now, opening an account, and let's say **you can't open an account on a Sunday** (as an example of a business rule):\n```go\nfunc Open(id AccountID, currentTime time.Time) (*Account, error) {\n\tif currentTime.Weekday() == time.Sunday {\n\t\treturn nil, errors.New(\"sorry, you can't open an account on Sunday ¯\\\\_(ツ)_/¯\")\n\t}\n\t// ...\n}\n```\n\nWe need a way to \"record\" this event, for that, we declare a helper, unexported method that uses `RecordEvent` from the `aggregate` package:\n```go\nfunc (a *Account) recordThat(event AccountEvent) error {\n\treturn aggregate.RecordEvent(a, event)\n}\n```\n\nGetting back to `Open`, recording an event is now straightforward:\n```go\nfunc Open(id AccountID, currentTime time.Time) (*Account, error) {\n\tif currentTime.Weekday() == time.Sunday {\n\t\treturn nil, errors.New(\"sorry, you can't open an account on Sunday ¯\\\\_(ツ)_/¯\")\n\t}\n\n\ta := NewEmpty()\n\n\t// Note: this is type safe, you'll get autocomplete for the events\n\tif err := a.recordThat(\u0026accountOpened{\n\t\tID:         id,\n\t\tOpenedAt:   currentTime,\n\t\tHolderName: holderName,\n\t}); err != nil {\n\t\treturn nil, fmt.Errorf(\"open account: %w\", err)\n\t}\n\n\treturn a, nil\n}\n```\n\nLet's add the other commands for our domain methods - I usually enforce business rules here:\n```go\nfunc (a *Account) DepositMoney(amount int) error {\n\tif amount \u003c= 0 {\n\t\treturn errors.New(\"amount must be greater than 0\")\n\t}\n\n\treturn a.recordThat(\u0026moneyDeposited{\n\t\tAmount: amount,\n\t})\n}\n```\n\nAnd withdrawing money:\n```go\n// Returns the amount withdrawn and an error if any\nfunc (a *Account) WithdrawMoney(amount int) (int, error) {\n\tif a.balance \u003c amount {\n\t\treturn 0, fmt.Errorf(\"insufficient money, balance left: %d\", a.balance)\n\t}\n\n\terr := a.recordThat(\u0026moneyWithdrawn{\n\t\tAmount: amount,\n\t})\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"error during withdrawal: %w\", err)\n\t}\n\n\treturn amount, nil\n}\n```\n\nThat's it, it's time to wire everything up.\n\nWe start by creating an event log. For this example, we'll use a simple in-memory log, but other implementations (sqlite, postgres etc.) are available.\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/DeluxeOwl/chronicle\"\n\t\"github.com/DeluxeOwl/chronicle/eventlog\"\n\t\"github.com/DeluxeOwl/chronicle/examples/internal/account\"\n\t\"github.com/sanity-io/litter\"\n)\n\nfunc main() {\n\t// Create a memory event log\n\tmemoryEventLog := eventlog.NewMemory()\n\t//...\n}\n```\n\nWe continue by creating the repository for the accounts:\n```go\n\taccountRepo, err := chronicle.NewEventSourcedRepository(\n\t\tmemoryEventLog,  // The event log\n\t\taccount.NewEmpty, // The constructor for our aggregate\n\t\tnil,             // This is an optional parameter called \"transformers\"\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n```\n\nWe create the account and interact with it\n```go\n\t// Create an account\n\tacc, err := account.Open(AccountID(\"123\"), time.Now(), \"John Smith\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t\n\t// Deposit some money\n\terr = acc.DepositMoney(200)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t\n\t// Withdraw some money\n\t_, err = acc.WithdrawMoney(50)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n```\n\nAnd we use the repo to save the account:\n```go\n\tctx := context.Background()\n\tversion, committedEvents, err := accountRepo.Save(ctx, acc)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n```\n\nThe repository returns the new version of the aggregate, the list of committed events, and an error if one occurred. The version is also updated on the aggregate instance itself and can be accessed via `acc.Version()` (this is handled by `aggregate.Base`)\n\nAn aggregate starts at version 0. The version is incremented for each new event that is recorded.\n\nPrinting these values gives:\n```go\n\tfmt.Printf(\"version: %d\\n\", version)\n\tfor _, ev := range committedEvents {\n\t\tlitter.Dump(ev)\n\t}\n```\n\n```go\n❯ go run examples/1_quickstart/main.go\nversion: 3\n\u0026main.accountOpened{\n  ID: \"123\",\n  OpenedAt: time.Time{}, // Note: litter omits private fields for brevity\n  HolderName: \"John Smith\",\n}\n\u0026main.moneyDeposited{\n  Amount: 200,\n}\n\u0026main.moneyWithdrawn{\n  Amount: 50,\n}\n```\n\nYou can find this example in [./examples/1_quickstart](./examples/1_quickstart).\nYou can find the implementation of the account in [./examples/internal/account/account.go](./examples/internal/account/account.go).\n\n**Note:** you will see an additional `accountv2` package that is 95% identical to the `account` package + shared event metadata. You can ignore this package as most examples assume the `account` package. You can find more info in the [Shared event metadata section](https://github.com/DeluxeOwl/chronicle?tab=readme-ov-file#shared-event-metadata).\n\n## What is event sourcing?\n\nEvent sourcing is a pattern for storing all changes to an application's state as a sequence of *immutable* \"**events**\".\n\nThe current state can be rebuilt from these events, treating the sequence (\"**event log**\") as the single source of truth.\n\nYou can only add new events to the event log; you can never change or delete existing ones.\n\nFor example, instead of storing a person's information in a conventional database table (like in PostgreSQL or SQLite):\n\n| id | name | age |\n| :--- | :--- | :-: |\n| 7d7e974e | John Smith | 2 |\n| 44bcdbc3 | Lisa Doe | 44 |\n\nWe store a sequence of events in an event log:\n\n| log_id | version | event_name | event_data |\n| :--- | :--- | :--- | :--- |\n| **person/7d7e974e** | **1** | **person/was_born** | **{\"name\": \"John Smith\"}** |\n| **person/7d7e974e** | **2** | **person/aged_one_year** | **{}** |\n| person/44bcdbc3 | 1 | person/was_born | {\"name\": \"Lisa Doe\"} |\n| **person/7d7e974e** | **3** | **person/aged_one_year** | **{}** |\n| person/44bcdbc3 | 2 | person/aged_one_year | {} |\n| ... | ... | ... | ... |\n\nBy **applying** (or replaying) these events in order for a, we can reconstruct the current state of any person.\n\nIn the example above, you would apply all events with the log ID `person/7d7e974e` (the bold rows), ordered by `version`, to reconstruct the current state for \"John Smith\".\n\nLet's take a simplified bank account example with the balance stored in an db table:\n\n| id | balance |\n| :--- | :--- |\n| 162accc9 | $150 |\n\nWith this model, if you see the balance is $150, you have no idea *how* it got there. The history is lost.\n\nWith event sourcing, the event log would instead store a list of all transactions:\n\n| log_id | version | event_name | event_data |\n| :--- | :--- | :--- | :--- |\n| account/162accc9 | 1 | account/created | {} |\n| account/162accc9 | 2 | account/money_deposited | {\"amount\": \"$200\"} |\n| account/162accc9 | 3 | account/money_withdrawn | {\"amount\": \"$50\"} |\n\nEvents are organized per log id (**also called an aggregate id**). In the examples above, you have events **per** person (`person/7d7e974e` and `person/44bcdbc3`) and **per** account (`account/162accc9`).\n\n\n\nEvents are facts: they describe *something* that happened in the past and should be named in the past tense:\n```\nperson/was_born\nperson/aged_one_year\naccount/money_deposited\naccount/money_withdrawn\n```\n\nYou might wonder, \"What if the user wants to see how many people are named 'John'?\" You'd have to replay ALL events for ALL people and count how many have the name \"John\".\n\nThat would be inefficient. This is why **projections** exist.\n\nProjections are specialized **read** models, optimized for querying. They are built by listening to the stream of events as they happen.\n\nExamples of projections:\n- How many people are named john\n- The people aged 30 to 40\n- How much money was withdrawn per day for the past 30 days\n\n\nProjections can be stored in many different ways, **usually separate** from the event log store itself:\n- In a database table you can query with SQL\n- In an in-memory database like Redis\n- In a search engine like Elasticsearch\n- Or simply in the application's memory\n\nFor example, let's create a projection that counts people named \"John\". Our projector is only interested in one event: `person/was_born`. It will ignore all others.\n\nHere’s how the projector builds the read model by processing events from the log one by one:\n\n| Incoming Event | Listener's Action | Projection State (our read model) |\n| :--- | :--- | :--- |\n| *(initial state)* | | `{ \"john_count\": 0 }` |\n| `person/was_born` `{\"name\": \"John Smith\"}` | Name starts with \"John\". `john_count` is incremented. | `{ \"john_count\": 1 }` |\n| `person/aged_one_year` `{}` | Irrelevant event for this projection. State is unchanged. | `{ \"john_count\": 1 }` |\n| `person/was_born` `{\"name\": \"Lisa Doe\"}` | Name does not start with \"John\". State is unchanged. | `{ \"john_count\": 1 }` |\n| `person/was_born` `{\"name\": \"John Doe\"}` | Name starts with \"John\". `john_count` is incremented. | `{ \"john_count\": 2 }` |\n| `person/aged_one_year` `{}` | Irrelevant event for this projection. State is unchanged. | `{ \"john_count\": 2 }` |\n| `person/was_born` `{\"name\": \"Peter Jones\"}` | Name does not start with \"John\". State is unchanged. | `{ \"john_count\": 2 }` |\n\nThe final result is a projection - a simple read model that's fast to query. \nIt could be stored in a key-value store like Redis, or a simple db table:\n\n**Table: `name_counts`**\n\n| name_search | person_count |\n| :--- | :--- |\n| john | 2 |\n| lisa | 1 |\n| peter | 1 |\n\nNow, when the end user asks, \"How many people are named John?\", you don't need to scan the entire event log. You simply query your projection, which gives you the answer instantly.\n\nThe event log is the source of truth, so projections can be rebuilt from it at any time. However, projections are usually updated *after* an event is written, which means they can briefly lag behind the state in the event log. This is known as **eventual consistency**.\n\nThis pattern plays well with Command Query Responsibility Segregation, or CQRS for short:\n- **Commands** write to the event log.\n- **Queries** read from projections.\n\n## Why event sourcing?\n\n\u003e [*\"Every system is a log\"*](https://news.ycombinator.com/item?id=42813049)\n\nHere are some of the most common benefits cited for event sourcing:\n- **Auditing**: You have a complete, unchangeable record of every action that has occurred in your application.\n- **Time Travel**: You can reconstruct the state of your application at any point in time.\n- **Read/Write Separation**: You can create new read models for new use cases at any time by replaying the event log, without impacting the write side.\n- **Scalability**: You can scale the read and write sides of your application independently.\n- **Simple integration**: Other systems can subscribe to the event stream.\n\nBut the main benefit I agree with comes from [this event-driven.io article](https://event-driven.io/en/dealing_with_eventual_consistency_and_idempotency_in_mongodb_projections/), paraphrasing:\n\u003e Event sourcing helps you first model *what happens* (the events), and **then** worry about how to interpret that data using projections.\n\nThe event log is a very powerful primitive, from [every system is a log](https://restate.dev/blog/every-system-is-a-log-avoiding-coordination-in-distributed-applications/) (I highly recommend reading this article and discussion on HN to get a better idea why a log is useful):\n- Message queues are logs: Apache Kafka, Pulsar, Meta’s Scribe are distributed implementations of the log abstraction. \n- Databases (and K/V stores) are logs: changes go to the write-ahead-log first, then get materialized into the tables.\n\n## Why not event sourcing?\n\nAdopting event sourcing is a significant architectural decision; it tends to influence the entire structure of your application (some might say it \"infects\" it).\n\nIn many applications, the current state of the data is all that matters.\n\nReasons **NOT** to use event sourcing:\n- **It can be massive overkill.** For simple CRUD applications without complex business rules, the current state of the data is often all that matters.\n    - For example, if your events are just `PersonCreated`, `PersonUpdated`, and `PersonDeleted`, you should seriously consider avoiding event sourcing.\n- **You don't want to deal with eventual consistency.** If your application requires immediate, strong consistency between writes and reads, event sourcing adds complexity.\n- **Some constraints are harder to enforce.**\n    - e.g. requiring unique usernames, see TODO\n- **Data deletion and privacy require complex workarounds.**\n    - e.g. the event log being immutable makes it hard to implement GDPR compliance, requiring things like [crypto shedding (see below)](https://github.com/DeluxeOwl/chronicle?tab=readme-ov-file#example-crypto-shedding-for-gdpr).\n- **It has a high learning curve.** Most developers are not familiar with this pattern.\n    - Forcing it on an unprepared team can lead to slower development and team friction.\n- **You cannot directly query the current state;** you must build and rely on projections for all queries.\n- **It often requires additional infrastructure,** such as a message queue (e.g., NATS, Amazon SQS, Kafka) to process events and update projections reliably.\n\n## Optimistic Concurrency \u0026 Conflict Errors\n\nYou can find this example in [./examples/2_optimistic_concurrency/main.go](./examples/2_optimistic_concurrency/main.go).\n\nWhat happens if two users try to withdraw money from the same bank account at the exact same time? This is called a \"race condition\" problem.\n\nEvent sourcing handles this using **Optimistic Concurrency Control**. \n\n`chronicle` handles this for you automatically thanks to the versioning system built into `aggregate.Base` - the struct you embed in your aggregates.\n\nWe're gonna use the `Account` example from the `examples/internal/account` package (check the quickstart if you haven't done so).\n\nWe're gonna open an account and deposit some money\n```go\n\tacc, _ := account.Open(accID, time.Now(), \"John Smith\")\n\t_ = acc.DepositMoney(200) // balance: 200\n\n\t_, _, _ = accountRepo.Save(ctx, acc)\n\tfmt.Printf(\"Initial account saved. Balance: 200, Version: %d\\n\\n\", acc.Version())\n```\n\nThe account starts at version 0, `Open` is event 1, `Deposit` is event 2.\nAfter saving, the version will be 2.\n\nThen, we assume two users load the same account at the same time:\n```go\n\taccUserA, _ := accountRepo.Get(ctx, accID)\n\tfmt.Printf(\"User A loads account. Version: %d, Balance: %d\\n\", accUserA.Version(), accUserA.Balance())\n\t// User A loads account. Version: 2, Balance: 200\n\n\taccUserB, _ := accountRepo.Get(ctx, accID)\n\tfmt.Printf(\"User B loads account. Version: %d, Balance: %d\\n\\n\", accUserB.Version(), accUserB.Balance())\n\t// User B loads account. Version: 2, Balance: 200\n```\n\nUser B tries to withdraw $50:\n```go\n\t_, _ = accUserB.WithdrawMoney(50)\n\t_, _, _ = accountRepo.Save(ctx, accUserB)\n\tfmt.Printf(\"User B withdraws $50 and saves. Account is now at version %d\\n\", accUserB.Version())\n```\n\nUser B withdraws $50 and **saves**. Account is now at version 3.\n\nAt the **same time**, User A tried to withdraw $100. The business logic passes because their copy of the account *thinks* the balance is still $200.\n\n```go\n\t_, _ = accUserA.WithdrawMoney(100)\n\tfmt.Println(\"User A tries to withdraw $100 and save...\")\n```\n\nUser A tries to save:\n```go\n\t_, _, err := accountRepo.Save(ctx, accUserA)\n\tif err != nil {\n\t\tvar conflictErr *version.ConflictError\n\t\tif errors.As(err, \u0026conflictErr) {\n\t\t\tfmt.Println(\"\\n💥 Oh no! A conflict error occurred!\")\n\t\t\tfmt.Printf(\"   User A's save failed because it expected version %d, but the actual version was %d.\\n\",\n\t\t\t\tconflictErr.Expected, conflictErr.Actual)\n\t\t} else {\n\t\t\t// Other save errors\n\t\t\tpanic(err)\n\t\t}\n\t}\n```\n\nAnd we get a **conflict error**: `User A's save failed because it expected version 2, but the actual version was 3.`\n\n### Handling conflict errors\n\nHow do you handle the error above? The most common way is to **retry the command**:\n1. Re-load the aggregate from the repository to get the absolute latest state and version\n2. Re-run the original command\n3. Try to Save again\n\n```go\n\t// 💥 Oh no! A conflict error occurred!\n\t//...\n\t//... try to withdraw again\n\taccUserA, _ = accountRepo.Get(ctx, accID)\n\t_, err = accUserA.WithdrawMoney(100)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(\"User A tries to withdraw $100 and save...\")\n\tversion, _, err := accountRepo.Save(ctx, accUserA)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Printf(\"User A saved successfully! Version is %d and balance $%d\\n\", version, accUserA.Balance())\n\t// User A saved successfully! Version is 4 and balance $50\n```\n\n\n\n### Retry with backoff\nThe retry cycle can be handled in a loop. If the conflicts are frequent, you might add a backoff.\n\nYou can wrap the repository with `chronicle.NewEventSourcedRepositoryWithRetry`, which uses github.com/avast/retry-go/v4 for retries. \n\n**The default is to retry 3 times on conflict errors**. You can customize the retry mechanism by providing `retry.Option(s)`.\n\n```go\n\timport \"github.com/avast/retry-go/v4\"\n\n\tar, _ := chronicle.NewEventSourcedRepository(\n\t\tmemoryEventLog,\n\t\taccount.NewEmpty,\n\t\tnil,\n\t)\n\n\taccountRepo := chronicle.NewEventSourcedRepositoryWithRetry(ar)\n\t\n\taccountRepo := chronicle.NewEventSourcedRepositoryWithRetry(ar, retry.Attempts(5),\n\t\tretry.Delay(100*time.Millisecond),    // Initial delay\n\t\tretry.MaxDelay(10*time.Second),       // Cap the maximum delay\n\t\tretry.DelayType(retry.BackOffDelay),  // Exponential backoff\n\t\tretry.MaxJitter(50*time.Millisecond), // Add randomness\n\t)\n```\n\n### Custom retry\n\nExample of wrapping a repository with a custom `Save` method with retries:\n\n```go\ntype SaveResult struct {\n\tVersion         version.Version\n\tCommittedEvents aggregate.CommittedEvents[account.AccountEvent]\n}\n\ntype SaverWithRetry struct {\n\tsaver aggregate.Saver[account.AccountID, account.AccountEvent, *account.Account]\n}\n\nfunc (s *SaverWithRetry) Save(ctx context.Context, root *account.Account) (version.Version, aggregate.CommittedEvents[account.AccountEvent], error) {\n\tresult, err := retry.DoWithData(\n\t\tfunc() (SaveResult, error) {\n\t\t\tversion, committedEvents, err := s.saver.Save(ctx, root)\n\t\t\tif err != nil {\n\t\t\t\treturn SaveResult{}, err\n\t\t\t}\n\t\t\treturn SaveResult{\n\t\t\t\tVersion:         version,\n\t\t\t\tCommittedEvents: committedEvents,\n\t\t\t}, nil\n\t\t},\n\t\tretry.Attempts(3),\n\t\tretry.Context(ctx),\n\t\tretry.RetryIf(func(err error) bool {\n\t\t\t// Only retry on ConflictErr or specific errors\n\t\t\tvar conflictErr *version.ConflictError\n\t\t\treturn errors.As(err, \u0026conflictErr)\n\t\t}),\n\t)\n\tif err != nil {\n\t\tvar zero version.Version\n\t\tvar zeroCE aggregate.CommittedEvents[account.AccountEvent]\n\t\treturn zero, zeroCE, err\n\t}\n\n\treturn result.Version, result.CommittedEvents, nil\n}\n// ...\n\taccountRepo, _ := chronicle.NewEventSourcedRepository(\n\t\tmemoryEventLog,\n\t\taccount.NewEmpty,\n\t\tnil,\n\t)\n\n\trepoWithRetry := \u0026aggregate.FusedRepo[account.AccountID, account.AccountEvent, *account.Account]{\n\t\tAggregateLoader: accountRepo,\n\t\tVersionedGetter: accountRepo,\n\t\tGetter:          accountRepo,\n\t\tSaver: \u0026SaverWithRetry{\n\t\t\tsaver: accountRepo,\n\t\t},\n\t}\n```\n\n\n### How is this different from SQL transactions?\n\nSQL transactions often use **pessimistic locking** - which means you assume a conflict is likely and you lock the data upfront to prevent it:\n\n1. **`BEGIN TRANSACTION;`**: Start a database transaction.\n2. **`SELECT balance FROM accounts WHERE id = 'acc-123' FOR UPDATE;`**: This is the critical step. The `FOR UPDATE` clause tells the database to place a **write lock** on the selected row.\n3. While this lock is held, no other transaction can read that row with `FOR UPDATE` or write to it. Any other process trying to do so will be **blocked** and forced to wait until the first transaction is finished.\n4. The application code checks if `balance \u003e= amount_to_withdraw`.\n5. **`UPDATE accounts SET balance = balance - 50 WHERE id = 'acc-123';`**: The balance is updated.\n6. **`COMMIT;`**: The transaction is committed, and the lock on the row is released.\n\nIn this model, a race condition like the one in our example is impossible. User B's transaction would simply pause at step 2, waiting for User A's transaction to `COMMIT` or `ROLLBACK`. \n\nThe database itself serializes the access. \n\nIn **optimistic concurrency control** - we shift the responsability from out database to our application.\n\n### Will conflicts be a bottleneck?\n\nA common question is: \"Will my app constantly handle conflict errors and retries? Won't that be a bottleneck?\".\n\nWith well designed aggregates, the answer is **no**. Conflicts are the exception, not the rule.\n\nThe most important thing to remember is that version conflicts happen **per aggregate**. \n\nA `version.ConflictError` for `AccountID(\"acc-123\")` has absolutely no effect on a concurrent operation for `AccountID(\"acc-456\")`. The aggregate itself defines the consistency boundary.\n\nBecause aggregates are typically designed to represent a single, cohesive entity that is most often manipulated by a single user at a time (like _your_ shopping cart, or _your_ user profile), the opportunity for conflicts is naturally low. \n\nThis fine-grained concurrency model is what allows event-sourced systems to achieve high throughput, as the vast majority of operations on different aggregates can proceed in parallel without any contention.\n\n## Snapshots\n\nYou can find this example in [./examples/3_snapshots/main.go](./examples/3_snapshots/main.go) and [(./examples/internal/account/account_snapshot.go](./examples/internal/account/account_snapshot.go).\n\nDuring the lifecycle of an application, some aggregates might accumulate a very long list of events.\n\nFor example, an account could exist for decades and accumulate thousands or even tens of thousands of transactions.\n\nLoading such an aggregate would require fetching and replaying its entire history from the beginning, which could become a performance bottleneck. (As a rule of thumb, always measure first-loading even a few thousand events is often perfectly acceptable performance-wise).\n\nSnapshots are a performance optimization that solves this problem. \n\nA snapshot is an encoded copy of an aggregate's state at a specific version. Instead of replaying the entire event history, the system can load the *latest snapshot* and then replay only the events that have occurred _since_ that snapshot was taken.\n\nLet's continue our `Account` example from the quickstart and add snapshot functionality. \n\nFirst we need to create our snapshot struct which needs to satisfy the interface `aggregate.Snapshot[TID]` (by implementing `ID() AccountID` and `Version() version.Version`). \n\nThis means it must store the aggregate's ID and version, which are then exposed via the required `ID()` and `Version()` methods.\n\n```go\npackage account\n\ntype Snapshot struct {\n\tAccountID        AccountID       `json:\"id\"`\n\tOpenedAt         time.Time       `json:\"openedAt\"`\n\tBalance          int             `json:\"balance\"`\n\tHolderName       string          `json:\"holderName\"`\n\tAggregateVersion version.Version `json:\"version\"`\n}\n\nfunc (s *Snapshot) ID() AccountID {\n\treturn s.AccountID\n}\n\nfunc (s *Snapshot) Version() version.Version {\n\treturn s.AggregateVersion\n}\n```\n\nIt's a \"snapshot\" (a point in time picture) of the `Account`'s state at a given point that can be encoded (JSON by default).\n\nNext, we need a way to convert an `Account` aggregate to an `AccountSnapshot` and back. This is the job of a `Snapshotter`. It acts as a bridge between your live aggregate and its encoded snapshot representation.\n\nWe create a type that implements the `aggregate.Snapshotter` interface.\n\n```go\npackage account\n\ntype Snapshotter struct{}\n\nfunc (s *Snapshotter) ToSnapshot(acc *Account) (*Snapshot, error) {\n\treturn \u0026Snapshot{\n\t\tAccountID:        acc.ID(), // Important: save the aggregate's id\n\t\tOpenedAt:         acc.openedAt,\n\t\tBalance:          acc.balance,\n\t\tHolderName:       acc.holderName,\n\t\tAggregateVersion: acc.Version(), // Important: save the aggregate's version\n\t}, nil\n}\n\nfunc (s *Snapshotter) FromSnapshot(snap *Snapshot) (*Account, error) {\n\t// Recreate the aggregate from the snapshot's data\n\tacc := NewEmpty()\n\tacc.id = snap.ID()\n\tacc.openedAt = snap.OpenedAt\n\tacc.balance = snap.Balance\n\tacc.holderName = snap.HolderName\n\n\t// ⚠️ The repository will set the correct version on the aggregate's Base\n\treturn acc, nil\n}\n```\n\nThe `ToSnapshot` method captures the current state, and `FromSnapshot` restores it. \n\nNote that `FromSnapshot` doesn't need to set the version on the aggregate's embedded `Base`; the framework handles this automatically when loading from a snapshot.\n\n\n\nLet's wire everything up in `main`:\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/DeluxeOwl/chronicle\"\n\t\"github.com/DeluxeOwl/chronicle/eventlog\"\n\t\"github.com/DeluxeOwl/chronicle/examples/internal/account\"\n\t\"github.com/DeluxeOwl/chronicle/snapshotstore\"\n)\n\nfunc main() {\n\tmemoryEventLog := eventlog.NewMemory()\n\tbaseRepo, _ := chronicle.NewEventSourcedRepository(\n\t\tmemoryEventLog,\n\t\taccount.NewEmpty,\n\t\tnil,\n\t)\n\n\taccountSnapshotStore := snapshotstore.NewMemory(\n\t\tfunc() *account.Snapshot { return new(account.Snapshot) },\n\t)\n\t// ...\n}\n```\n\nWe create a snapshot store for our snapshots. It needs a constructor for an empty snapshot, used for decoding.\n\nWhile the library provides an in-memory snapshot store out of the box, the `aggregate.SnapshotStore` interface makes it straightforward to implement your own persistent store (e.g., using a database table, Redis, or a file-based store).\n\nThen, we wrap the base repository with the snapshotting functionality. \n\nNow, we need to decide _when_ to take a snapshot. You probably don't want to create one on every single change, as that would be inefficient. The framework provides a flexible `SnapshotPolicy` to define this policy and a builder for various policies:\n```go\n\taccountSnapshotStore := snapshotstore.NewMemory(\n\t\tfunc() *account.Snapshot { return new(account.Snapshot) },\n\t)\n\n\taccountRepo, err := chronicle.NewEventSourcedRepositoryWithSnapshots(\n\t\tbaseRepo,\n\t\taccountSnapshotStore,\n\t\t\u0026account.Snapshotter{},\n\t\taggregate.SnapPolicyFor[*account.Account]().EveryNEvents(3),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n```\n\nIn our example, we chose to snapshot every 3 events.\n\nLet's issue some commands:\n```go\n\tctx := context.Background()\n\taccID := account.AccountID(\"snap-123\")\n\n\tacc, err := account.Open(accID, time.Now(), \"John Smith\") // version 1\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t_ = acc.DepositMoney(100) // version 2\n\t_ = acc.DepositMoney(100) // version 3\n\t\n\t// Saving the aggregate with 3 uncommitted events.\n\t// The new version will be 3.\n\t// Since 3 \u003e= 3 (our N), the policy will trigger a snapshot.\n\t_, _, err = accountRepo.Save(ctx, acc)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n```\n\nThe repository loads the snapshot at version 3. Then, it will ask the event log for events for \"snap-123\" starting from version 4. Since there are none, loading is complete, and very fast.\n\n```go\n\treloadedAcc, err := accountRepo.Get(ctx, accID)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Printf(\"Loaded account from snapshot. Version: %d\\n\", reloadedAcc.Version())\n\t// Loaded account from snapshot. Version: 3\n```\n\nWe can also get the snapshot from the store to check:\n```go\n\tsnap, found, err := accountSnapshotStore.GetSnapshot(ctx, accID)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Printf(\"Found snapshot: %t: %+v\\n\", found, snap)\n\t// Found snapshot: true: \u0026{AccountID:snap-123 OpenedAt:2025-08-25 10:53:57.970965 +0300 EEST Balance:200 HolderName:John Smith AggregateVersion:3}\n```\n\n### Snapshot policies\nIf you type `aggregate.SnapPolicyFor[*account.Account]().` you will get autocomplete for various snapshot policies:\n- `EveryNEvents(n)`: Takes a snapshot every n times the aggregate is saved.\n- `AfterCommit()`: Takes a snapshot after every successful save.\n- `OnEvents(eventNames...)`: Takes a snapshot only if one or more of the specified event types were part of the save.\n- `AllOf(policies...)`: A composite policy that triggers only if all of its child policies match.\n- `AnyOf(policies...)`: A composite policy that triggers if any of its child policies match.     \n\nOr a `Custom(...)` policy, which gives you complete control by allowing you to provide your own function. This function receives the aggregate's state, its versions, and the list of committed events, so you can decide when a snapshot should be taken:\n```go\naggregate.SnapPolicyFor[*account.Account]().Custom(\n\t\t\tfunc(ctx context.Context, root *account.Account, previousVersion, newVersion version.Version, committedEvents aggregate.CommittedEvents[account.AccountEvent]) bool {\n\t\t\t\treturn true // always snapshot\n\t\t\t}),\n```\n\nAn example in [account_snapshot.go](./examples/internal/account/account_snapshot.go):\n```go\nfunc CustomSnapshot(\n\tctx context.Context,\n\troot *Account,\n\t_, _ version.Version,\n\t_ aggregate.CommittedEvents[AccountEvent],\n) bool {\n\treturn root.balance%250 == 0 // Only snapshot if the balance is a multiple of 250\n}\n```\n\n**Important**: Regardless of the snapshot policy chosen, saving the snapshot happens after the new events are successfully committed to the event log. This means the two operations are not atomic. It is possible for the events to be saved successfully but for the subsequent snapshot save to fail. \n\nBy default, an error during a snapshot save will be returned by the Save method. You can customize this behavior with the `aggregate.OnSnapshotError` option, allowing you to log the error and continue, or ignore it entirely.\n\nSince snapshots are purely a performance optimization, ignoring a failed snapshot save can be a safe and reasonable strategy. The aggregate can always be rebuilt from the event log, which remains the single source of truth.\n\n## Shared event metadata\n\nYou can find this example in [./examples/5_event_metadata](./examples/5_event_metadata/main.go), [./examples/internal/shared](./examples/internal/shared/event.go) and [./examples/internal/accountv2](./examples/internal/accountv2/).\n\nYou might be interested in sharing some fields between events: a timestamp, an event id, correlation ids, some authorization data (like who triggered an event) etc.\n\nThis kind of data is very useful for projections: like getting the events in the past 30 days.\n\nTechnically you could add this metadata at the `event.Log` layer, but I prefer adding it at the application layer.\n\nWe want our events to have the following: a unique id and a timestamp.\nFor that, we're going to create a shared event that must be embedded by our `AccountEvent`(s).\n\n```go\n// in examples/internal/shared/event.go\npackage shared\n\ntype EventMeta interface {\n\tisEventMeta()\n}\n\ntype EventMetadata struct {\n\tEventID   string    `json:\"eventID\"`\n\tOccuredAt time.Time `json:\"occuredAt\"`\n}\n\nfunc (em *EventMetadata) isEventMeta() {}\n```\n\nWe're going to use the `EventMeta` sealed interface as our compile-time check to remind us to embed the metadata.\n\nWe'll create a constructor that will generate `EventMetadata` for us:\n```go\nfunc NewEventMetaGenerator(provider timeutils.TimeProvider) *EventMetaGenerator {\n\treturn \u0026EventMetaGenerator{\n\t\tgen: func() EventMetadata {\n\t\t\tnow := provider.Now()\n\n\t\t\treturn EventMetadata{\n\t\t\t\t// The uuidv7 contains the timestamp\n\t\t\t\tEventID: uuid.Must(uuid.NewV7AtTime(now)).String(),\n\t\t\t\t// Or just same a simple timestamp\n\t\t\t\tOccuredAt: now,\n\t\t\t}\n\t\t},\n\t}\n}\n\ntype EventMetaGenerator struct {\n\tgen func() EventMetadata\n}\n\nfunc (gen *EventMetaGenerator) NewEventMeta() EventMetadata {\n\treturn gen.gen()\n}\n```\n\nWait, what's that `timeutils.TimeProvider` type? That type is an interface which helps us mock the time for testing purposes:\n```go\npackage timeutils\n\nimport \"time\"\n\n//go:generate go run github.com/matryer/moq@latest -pkg timeutils -skip-ensure -rm -out now_mock.go . TimeProvider\ntype TimeProvider interface {\n\tNow() time.Time\n}\n\nvar RealTimeProvider = sync.OnceValue(func() *realTimeProvider {\n\treturn \u0026realTimeProvider{}\n})\n\ntype realTimeProvider struct{}\n\nfunc (r *realTimeProvider) Now() time.Time {\n\treturn time.Now()\n}\n```\n\nMoving on, we're going to wire this event metadata into our account. We've created a new package called `accountv2` that we're going to extend with the metadata.\n\nLet's add our `shared.EventMeta` interface to our `AccountEvent`\n```go\n//sumtype:decl\ntype AccountEvent interface {\n\tevent.Any\n\tshared.EventMeta\n\tisAccountEvent()\n}\n```\n\nWe're gonna get some compiler errors:\n```\ncannot use new(accountOpened) (value of type *accountOpened) as AccountEvent value in return statement: *accountOpened does not implement AccountEvent (missing method isEventMeta)\n```\n\nThese errors tell use that we're missing the method `isEventMeta()`, a method that is only satisfied by the `shared.EventMetadata` struct.\nThis acts as our compile-time check, telling us that we must embed the `shared.EventMetadata` struct to our events.\n\n```go\ntype accountOpened struct {\n\tshared.EventMetadata\n\tID         AccountID `json:\"id\"`\n\tOpenedAt   time.Time `json:\"openedAt\"`\n\tHolderName string    `json:\"holderName\"`\n}\n// ...\ntype moneyDeposited struct {\n\tshared.EventMetadata\n\tAmount int `json:\"amount\"`\n}\n// ...\ntype moneyWithdrawn struct {\n\tshared.EventMetadata\n\tAmount int `json:\"amount\"`\n}\n```\n\nNow we've got the `exhaustruct` linter complaining:\n```go\nfunc Open(id AccountID, currentTime time.Time, holderName string) (*Account, error) {\n\tif currentTime.Weekday() == time.Sunday {\n\t\treturn nil, errors.New(\"sorry, you can't open an account on Sunday ¯\\\\_(ツ)_/¯\")\n\t}\n\n\ta := NewEmpty()\n\n\t// ⚠️ accountv2.accountOpened is missing field EventMetadata (exhaustruct)\n\tif err := a.recordThat(\u0026accountOpened{\n\t\tID:         id,\n\t\tOpenedAt:   currentTime,\n\t\tHolderName: holderName,\n\t}); err != nil {\n\t\treturn nil, fmt.Errorf(\"open account: %w\", err)\n\t}\n\n\treturn a, nil\n}\n```\n\nWe have to modify our code a bit, we're not going to pass the `currentTime` to our `Open(...)` function, instead we'll pass a `timeutils.TimeProvider`.\n\nSome purists might say that this pollutes our domain model, but I believe injecting some dependencies is a pragmatic approach that helps us with testing.\n```go\ntype Account struct {\n\taggregate.Base\n\t// ...\n\n\t// technical dependencies\n\ttimeProvider  timeutils.TimeProvider\n\tmetaGenerator *shared.EventMetaGenerator\n}\n\nfunc Open(id AccountID, timeProvider timeutils.TimeProvider, holderName string) (*Account, error) {\n\t// ...\n}\n```\n\nWe also have to change our empty constructor (the one used by the repositories) to account for these technical dependencies:\n```go\n// From this\n\nfunc NewEmpty() *Account {\n\treturn new(Account)\n}\n\n// To this\nfunc NewEmptyMaker(timeProvider timeutils.TimeProvider) func() *Account {\n\treturn func() *Account {\n\t\treturn \u0026Account{\n\t\t\ttimeProvider:  timeProvider,\n\t\t\tmetaGenerator: shared.NewEventMetaGenerator(timeProvider),\n\t\t}\n\t}\n}\n```\n\nWe have to return a `func() *Account` since repositories expect this type.\n\nAdding this to our `Open(...)` function and using the generator to generate the metadata:\n```go\nfunc Open(id AccountID, timeProvider timeutils.TimeProvider, holderName string) (*Account, error) {\n\tmakeEmptyAccount := NewEmptyMaker(timeProvider) // Create the maker with the dependencies\n\ta := makeEmptyAccount()\n\n\tcurrentTime := a.timeProvider.Now()\n\n\tif currentTime.Weekday() == time.Sunday {\n\t\treturn nil, errors.New(\"sorry, you can't open an account on Sunday ¯\\\\_(ツ)_/¯\")\n\t}\n\n\tif err := a.recordThat(\u0026accountOpened{\n\t\tID:            id,\n\t\tOpenedAt:      currentTime,\n\t\tHolderName:    holderName,\n\t\tEventMetadata: a.metaGenerator.NewEventMeta(), // We're using the generator to generate the metadata\n\t}); err != nil {\n\t\treturn nil, fmt.Errorf(\"open account: %w\", err)\n\t}\n\n\treturn a, nil\n}\n```\n\nLet's update the other places where we're generating events:\n```go\nerr := a.recordThat(\u0026moneyWithdrawn{\n\t\tAmount:        amount,\n\t\tEventMetadata: a.metaGenerator.NewEventMeta(),\n\t})\n// ...\nreturn a.recordThat(\u0026moneyDeposited{\n\t\tAmount:        amount,\n\t\tEventMetadata: a.metaGenerator.NewEventMeta(),\n\t})\n```\n\nOur compiler is complaining that it doesn't find the old `NewEmpty` function in the snapshot:\n```go\nfunc (s *Snapshotter) FromSnapshot(snap *Snapshot) (*Account, error) {\n\tacc := NewEmpty() // ⚠️ Not found\n\tacc.id = snap.ID()\n\tacc.openedAt = snap.OpenedAt\n\t// ...\n\treturn acc, nil\n}\n```\n\nThe way to fix this is to provide the same time provider dependency to the `Snapshotter`:\n```go\ntype Snapshotter struct {\n\tTimeProvider timeutils.TimeProvider\n}\n\nfunc (s *Snapshotter) FromSnapshot(snap *Snapshot) (*Account, error) {\n\t// Recreate the aggregate from the snapshot's data\n\tacc := NewEmptyMaker(s.TimeProvider)() // ✅ Create the maker and call it\n\tacc.id = snap.ID()\n\t// ...\n}\n```\n\nWe're going to use the snapshot example but adapt it to account for our shared metadata. You can find this in [examples/5_event_metadata](./examples/5_event_metadata/main.go).\n\nWe're gonna use a mocked time, generating the current time and setting the year to 2100 (we're in 2100 baby).\n```go\nfunc main() {\n\tmemoryEventLog := eventlog.NewMemory()\n\n\t// We're using a mock time provider, generating the current time but setting the year to 2100\n\ttimeProvider := \u0026timeutils.TimeProviderMock{\n\t\tNowFunc: func() time.Time {\n\t\t\tnow := time.Now()\n\n\t\t\tfutureTime := now.AddDate(2100-now.Year(), 0, 0)\n\n\t\t\treturn futureTime\n\t\t},\n\t}\n\taccountMaker := accountv2.NewEmptyMaker(timeProvider) // Create the maker\n\n\tbaseRepo, _ := chronicle.NewEventSourcedRepository(\n\t\tmemoryEventLog,\n\t\taccountMaker, // Pass it into the repo\n\t\tnil,\n\t)\n\t// ...\n}\n```\n\nIt's really important to pass the same `timeProvider` to the snapshotter:\n```go\n\taccountRepo, err := chronicle.NewEventSourcedRepositoryWithSnapshots(\n\t\t\tbaseRepo,\n\t\t\taccountSnapshotStore,\n\t\t\t\u0026accountv2.Snapshotter{\n\t\t\t\tTimeProvider: timeProvider, // ⚠️ The same timeProvider\n\t\t\t},\n\t\t\taggregate.SnapPolicyFor[*accountv2.Account]().EveryNEvents(3),\n\t\t)\n```\n\nAnd in our `accountv2.Open`:\n```go\n\tacc, err := accountv2.Open(accID, timeProvider, \"John Smith\") // ⚠️ The same timeProvider\n\t// ...\n\t// Print the events\n\tfor ev := range memoryEventLog.ReadAllEvents(ctx, version.SelectFromBeginning) {\n\t\tfmt.Println(ev.EventName() + \" \" + string(ev.Data()))\n\t}\n```\n\nRunning the example:\n```bash\ngo run examples/5_event_metadata/main.go\n\nLoaded account from snapshot. Version: 3\nFound snapshot: true: \u0026{AccountID:snap-123 OpenedAt:2100-08-27 11:20:56.239593 +0300 EEST Balance:200 HolderName:John Smith AggregateVersion:3}\n\naccount/opened {\"eventID\":\"03bff837-ff2f-7d26-a69e-1a75693570ab\",\"occuredAt\":\"2100-08-27T11:20:56.239656+03:00\",\"id\":\"snap-123\",\"openedAt\":\"2100-08-27T11:20:56.239593+03:00\",\"holderName\":\"John Smith\"}\naccount/money_deposited {\"eventID\":\"03bff837-ff2f-7d27-a50b-a81c5f7a33eb\",\"occuredAt\":\"2100-08-27T11:20:56.239665+03:00\",\"amount\":100}\naccount/money_deposited {\"eventID\":\"03bff837-ff2f-7d28-8c54-9594563b48b4\",\"occuredAt\":\"2100-08-27T11:20:56.239666+03:00\",\"amount\":100}\n```\n\n\n## Ordering: Event log \u0026 Global event log\n\nThe core guarantee of any event log is that events for a single aggregate are stored and retrieved in the exact order they occurred. When you load `AccountID(\"123\")`, you get an ordered history for that account. This is the contract provided by the base `event.Log` interface.\n\nThis is sufficient for rehydrating an aggregate or for building projections that only care about a single stream of events at a time. \n\nHowever, some use cases require a guaranteed, chronological order of events across the _entire system_. For example, building a system-wide audit trail or a projection that aggregates data from different types of aggregates requires knowing if `account/opened` for User A happened before or after `order/placed` for User B.\n\nIt also makes building the kind of projections where you need multiple aggregates (like seeing total money withdrawn for every `Account`) easier.\n\nThis is where the `event.GlobalLog` interface comes in. It extends `event.Log` with the ability to read a single, globally ordered stream of all events. \nBackends like the `Postgres` and `Sqlite` logs implement this by assigning a unique, monotonically increasing `global_version` to every event that is committed, in addition to its per-aggregate `version`.\n\n```go\n// event/event_log.go\n\n// GlobalLog extends a standard Log with the ability to read all events across\n// all aggregates, in the global order they were committed.\ntype GlobalLog interface {\n    Log\n    GlobalReader\n}\n\n// GlobalReader defines the contract for reading the global stream.\ntype GlobalReader interface {\n    ReadAllEvents(ctx context.Context, globalSelector version.Selector) GlobalRecords\n}\n```\n\n\u003e [!IMPORTANT] \n\u003e This distinction exists because not all storage backends can efficiently provide a strict global ordering.\n\n- **SQL Databases**: A database like PostgreSQL or SQLite can easily generate a global order using an `IDENTITY` or `AUTOINCREMENT` primary key on the events table. The provided SQL-based logs implement `event.GlobalLog`.\n- **Distributed Systems**: A distributed message queue like Apache Kafka guarantees strict ordering only _within a topic partition_. If each aggregate ID were mapped to a partition, you would have perfect per-aggregate order, but no simple, built-in global order. An event log built on kafka would likely only implement the base `event.Log` interface.\n- **Key-Value Stores**: A store like Pebble can also implement `GlobalLog` by maintaining a secondary index for the global order. While reading from this index is fast, a `Pebble` implementation might make a trade-off for simplicity: it uses a global lock during writes to safely assign the next `global_version`. This serializes all writes to the event store, which can become a performance bottleneck under high concurrent load. Note: Pebble was removed in [this commit](https://github.com/DeluxeOwl/chronicle/commit/823463288633728a3ea3282384f73ff746b5a866). The reason was that it's inneficient and nobody used it. A better KV store might be bolt.\n\nChronicle's separate interfaces acknowledge this reality, allowing you to choose a backend that fits your application's consistency and projection requirements. If you need to build projections that rely on the precise, system-wide order of events, you should choose a backend that supports `event.GlobalLog`.\n\n## Storage backends\n\n### Event logs\n\n- **In-Memory**: `eventlog.NewMemory()`\n    - The simplest implementation. It stores all events in memory.\n    - **Use Case**: Ideal for quickstarts, examples, and running tests.\n    - **Tradeoff**: It is not persistent. All data is lost when the application restarts. It can only be used within a single process.\n- **SQLite \u0026 PostgreSQL**: `eventlog.NewSqlite(db)` and `eventlog.NewPostgres(db)`\n    - These are robust, production-ready implementations that leverage SQL databases for persistence. PostgreSQL is ideal for distributed, high-concurrency applications, while SQLite is a great choice for single-server deployments.\n    - **Use Case**: Most of them.\n    - **Tradeoff**: It requires you to deal with scaling - so they might not be the best if you're dealing with billions of events per second.\n\nA key feature of these SQL-based logs is how they handle optimistic concurrency. Instead of relying on application-level checks, they use **database triggers** to enforce version consistency.\n\nWhen you try to append an event, a trigger fires inside the database. It atomically checks if the new event's version is exactly one greater than the stream's current maximum version.\n\nIf the versions don't line up, the database transaction itself fails and raises an exception, preventing race conditions.\n\n\u003e [!NOTE] \n\u003e To ensure the framework can correctly identify a conflict regardless of the database driver used, the triggers are designed to raise a specific error message: `_chronicle_version_conflict: \u003cactual_version\u003e`. The framework parses this string to create a `version.ConflictError`, making the conflict driver agnostic.\n\n### Snapshot stores\n\n- **In-Memory**: `snapshotstore.NewMemory(...)`\n    - Stores snapshots in a simple map. Perfect for testing.\n    - **Tradeoff**: Single process only, lost on restart.\n- **PostgreSQL**: `snapshotstore.NewPostgres(...)`\n    - A persistent implementation that stores snapshots in a PostgreSQL table using an atomic `INSERT ... ON CONFLICT DO UPDATE` statement.\n    - **Use Case**: When you need durable snapshots.\n\nYou can create your own snapshot store for other databases (like SQLite or Redis) by implementing the `aggregate.SnapshotStore` interface.\n\nSaving a snapshot is usually an \"UPSERT\" (update or insert) operation.\n\n## Event transformers\nYou can find this example in in [./examples/4_transformers_crypto/main.go](./examples/4_transformers_crypto/main.go) and [account_aes_crypto_transformer.go](./examples/internal/account/account_aes_crypto_transformer.go).\n\nTransformers allow you to modify events just before they are encoded and written to the event log, and right after they are read and decoded. This is a powerful hook for implementing cross-cutting concerns.\n\nCommon use cases include:\n\n- **Encryption**: Securing sensitive data within your events.\n- **Compression**: Reducing the storage size of large event payloads.\n- **Up-casting**: Upgrading older versions of an event to a newer schema on the fly.\n\nWhen you provide multiple transformers, they are applied in the order you list them for writing, and in the **reverse order** for reading. For example: `encrypt -\u003e compress` on write becomes `decompress -\u003e decrypt` on read.\n\n### Example: Crypto shedding for GDPR\n\nOur account has the holder name field, which we consider PII (Personally Identifiable Information) and we'd like to have it encrypted.\n```go\ntype Account struct {\n\taggregate.Base\n\n\tid AccountID\n\n\topenedAt   time.Time\n\tbalance    int\n\tholderName string // We assume this is PII (Personally Identifiable Information)\n}\n```\n\nGenerated by the event\n```go\ntype accountOpened struct {\n\tID         AccountID `json:\"id\"`\n\tOpenedAt   time.Time `json:\"openedAt\"`\n\tHolderName string    `json:\"holderName\"`\n}\n\nfunc (*accountOpened) EventName() string { return \"account/opened\" }\nfunc (*accountOpened) isAccountEvent()   {}\n```\n\nWe'll create a `CryptoTransformer` that implements the `event.Transformer[E]` interface to encrypt our . For this example, we'll use AES-GCM.\n\nWe define our encrypt and decrypt helper functions in the `account` package. \n```go\npackage account\n\nfunc encrypt(plaintext []byte, key []byte) ([]byte, error) {\n\t// ...\n}\n\nfunc decrypt(ciphertext []byte, key []byte) ([]byte, error) {\n\t// ...\n}\n```\n\nAnd let's define our crypto transformer:\n```go\ntype CryptoTransformer struct {\n\tkey []byte\n}\n\nfunc NewCryptoTransformer(key []byte) *CryptoTransformer {\n\treturn \u0026CryptoTransformer{\n\t\tkey: key,\n\t}\n}\n\nfunc (t *CryptoTransformer) TransformForWrite(\n\tctx context.Context,\n\tevents []AccountEvent,\n) ([]AccountEvent, error) {\n\tfor _, event := range events {\n\t\tif opened, isOpened := event.(*accountOpened); isOpened {\n\t\t\tfmt.Println(\"Received \\\"accountOpened\\\" event\")\n\n\t\t\tencryptedName, err := encrypt([]byte(opened.HolderName), t.key)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to encrypt holder name: %w\", err)\n\t\t\t}\n\t\t\topened.HolderName = base64.StdEncoding.EncodeToString(encryptedName)\n\n\t\t\tfmt.Printf(\"Holder name after encryption and encoding: %s\\n\", opened.HolderName)\n\t\t}\n\t}\n\n\treturn events, nil\n}\n```\n\nWe're iterating through the events and checking on write if the event is of type `*accountOpened`. If it is, we're encrypting the name and encoding it to base64.\n\nThe `TransformForRead` method should be the inverse of this process:\n```go\nfunc (t *CryptoTransformer) TransformForRead(\n\tctx context.Context,\n\tevents []AccountEvent,\n) ([]AccountEvent, error) {\n\tfor _, event := range events {\n\t\tif opened, isOpened := event.(*accountOpened); isOpened {\n\t\t\tfmt.Printf(\"Holder name before decoding: %s\\n\", opened.HolderName)\n\n\t\t\tdecoded, err := base64.StdEncoding.DecodeString(opened.HolderName)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to decode encrypted name: %w\", err)\n\t\t\t}\n\n\t\t\tfmt.Printf(\"Holder name before decryption: %s\\n\", decoded)\n\t\t\tdecryptedName, err := decrypt(decoded, t.key)\n\t\t\tif err != nil {\n\t\t\t\t// This happens if the key is wrong (or \"deleted\")\n\t\t\t\treturn nil, fmt.Errorf(\"failed to decrypt holder name: %w\", err)\n\t\t\t}\n\t\t\topened.HolderName = string(decryptedName)\n\t\t\tfmt.Printf(\"Holder name after decryption: %s\\n\", opened.HolderName)\n\t\t}\n\t}\n\n\treturn events, nil\n}\n```\n\nLet's wire everything up:\n```go\nfunc main() {\n\tmemoryEventLog := eventlog.NewMemory()\n\n\t// A 256-bit key (32 bytes)\n\tencryptionKey := []byte(\"a-very-secret-32-byte-key-123456\")\n\tcryptoTransformer := account.NewCryptoTransformer(encryptionKey)\n\t// ...\n}\n```\n\n\u003e [!WARNING] \n\u003e In a real production system, you must manage encryption keys securely using a Key Management Service (KMS) like AWS KMS, Google Cloud KMS, HashiCorp Vault or OpenBao. The key should never be hardcoded. You'd also manage one encryption key per aggregate ideally, in our simple example, we're using a hardcoded key for all accounts.\n\nAdd the transformer:\n```go\n\taccountRepo, err := chronicle.NewEventSourcedRepository(\n\t\tmemoryEventLog,\n\t\taccount.NewEmpty,\n\t\t[]event.Transformer[account.AccountEvent]{cryptoTransformer},\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n```\n\nCreate an account and deposit some money:\n```go\n\tctx := context.Background()\n\taccID := account.AccountID(\"crypto-123\")\n\n\tacc, err := account.Open(accID, time.Now(), \"John Smith\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_ = acc.DepositMoney(100)\n\t_, _, err = accountRepo.Save(ctx, acc)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Printf(\"Account for '%s' saved successfully.\\n\", acc.HolderName())\n\n\t// 4. Load the account again to verify decryption\n\treloadedAcc, err := accountRepo.Get(ctx, accID)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Printf(\"Account reloaded. Holder name is correctly decrypted: '%s'\\n\\n\", reloadedAcc.HolderName())\n```\n\nRunning the example prints:\n```go\n❯ go run examples/4_transformers_crypto/main.go\nReceived \"accountOpened\" event\nHolder name after encryption and encoding: +hWTqlo9VQBUXBJGFsfJouv5eL3PziMwd+gWhVkbtYbaH1CDbLw=\nAccount for 'John Smith' saved successfully.\nHolder name before decoding: +hWTqlo9VQBUXBJGFsfJouv5eL3PziMwd+gWhVkbtYbaH1CDbLw=\nHolder name before decryption: ���Z=UT\\F�ɢ��x���#0w��Y��P�l�\nHolder name after decryption: John Smith\nAccount reloaded. Holder name is correctly decrypted: 'John Smith'\n```\n\nFinally, let's simulate a GDPR \"right to be forgotten\" request. We'll \"delete\" the key and create a new repository with a new transformer using a different key. Attempting to load the aggregate will now fail because the data cannot be decrypted.\n\n```go\n\tfmt.Println(\"!!!! Simulating GDPR request: Deleting the encryption key. !!!!\")\n\n\tdeletedKey := []byte(\"a-very-deleted-key-1234567891234\")\n\tcryptoTransformer = account.NewCryptoTransformer(deletedKey)\n\n\tforgottenRepo, _ := chronicle.NewEventSourcedRepository(\n\t\tmemoryEventLog,\n\t\taccount.NewEmpty,\n\t\t[]event.Transformer[account.AccountEvent]{cryptoTransformer},\n\t)\n\t_, err = forgottenRepo.Get(context.Background(), accID)\n\tif err != nil {\n\t\tfmt.Printf(\"Success! The data is unreadable. Error: %v\\n\", err)\n\t}\n```\n\nRunning the example again prints:\n```go\n❯ go run examples/4_transformers_crypto/main.go\nReceived \"accountOpened\" event\nHolder name after encryption and encoding: +hWTqlo9VQBUXBJGFsfJouv5eL3PziMwd+gWhVkbtYbaH1CDbLw=\nAccount for 'John Smith' saved successfully.\nHolder name before decoding: +hWTqlo9VQBUXBJGFsfJouv5eL3PziMwd+gWhVkbtYbaH1CDbLw=\nHolder name before decryption: ���Z=UT\\F�ɢ��x���#0w��Y��P�l�\nHolder name after decryption: John Smith\nAccount reloaded. Holder name is correctly decrypted: 'John Smith'\n\n!!!! Simulating GDPR request: Deleting the encryption key. !!!!\nHolder name before decoding: +hWTqlo9VQBUXBJGFsfJouv5eL3PziMwd+gWhVkbtYbaH1CDbLw=\nHolder name before decryption: ���Z=UT\\F�ɢ��x���#0w��Y��P�l�\nSuccess! The data is unreadable. Error: repo get version 0: read and load from store: load from records: read transform for event \"account/opened\" (version 1) failed: failed to decrypt holder name: cipher: message authentication failed\n```\n\n### Global Transformers with `AnyTransformerToTyped`\n\nWhile the `CryptoTransformer` is specific to `account.AccountEvent`, you might want to create a generic transformer that can operate on events from any aggregate. For example, a global logging mechanism.\n\nThis is where `event.AnyTransformer` is useful. It is a type alias for `event.Transformer[event.Any]`, allowing it to process any event in the system as long as it satisfies the base `event.Any` interface.\n\nLet's create a simple transformer that logs every event being written to or read from the event log.\n```go\n// in examples/4_transformers/main.go\n\ntype LoggingTransformer struct{}\n\n// This transformer works with any event type (`event.Any`).\nfunc (t *LoggingTransformer) TransformForWrite(\n\t_ context.Context,\n\tevents []event.Any,\n) ([]event.Any, error) {\n\tfor _, event := range events {\n\t\tfmt.Printf(\"[LOG] Writing event: %s\\n\", event.EventName())\n\t}\n\n\treturn events, nil\n}\n\nfunc (t *LoggingTransformer) TransformForRead(\n\t_ context.Context,\n\tevents []event.Any,\n) ([]event.Any, error) {\n\tfor _, event := range events {\n\t\tfmt.Printf(\"[LOG] Reading event: %s\\n\", event.EventName())\n\t}\n\treturn events, nil\n}\n```\n\nHowever, a repository for a specific aggregate, like our `accountRepo`, expects a `[]event.Transformer[account.AccountEvent]`, not a `[]event.Transformer[event.Any]`. A direct assignment will fail due to Go's type system.\n\n`AnyTransformerToTyped` is a helper function that solves this. It's an adapter that takes your generic `AnyTransformer` and makes it compatible with a specific aggregate's repository.\n\nHere is how you would use both our specific `CryptoTransformer` and our global `LoggingTransformer` for the account repository.\n\n```go\n\tcryptoTransformer = account.NewCryptoTransformer(deletedKey)\n\n\tloggingTransformer := \u0026LoggingTransformer{}\n\n\tforgottenRepo, _ := chronicle.NewEventSourcedRepository(\n\t\tmemoryEventLog,\n\t\taccount.NewEmpty,\n\t\t[]event.Transformer[account.AccountEvent]{\n\t\t\tcryptoTransformer,\n\t\t\tevent.AnyTransformerToTyped[account.AccountEvent](loggingTransformer),\n\t\t},\n\t)\n```\n\n### Event versioning and upcasting\n\nYou can find this example in [aggregate_upcasting_test.go](./aggregate/aggregate_upcasting_test.go).\n\nAs your application evolves, so will your events. You might need to rename fields, change data types, or split one event into several more granular ones. Since the event log is immutable, you can't go back and change historical events. This is where event upcasting comes in. \n\nUpcasting is the process of transforming an older version of an event into its newer equivalent, on-the-fly, as it's read from the event store. `chronicle` handles this using the same `event.Transformer` interface.\n\nImagine we started with a single event to update a person's name and age: \n```go\n// V1 event - written in the past\ntype nameAndAgeSetV1 struct {\n    Name string `json:\"name\"`\n    Age  int    `json:\"age\"`\n}\nfunc (*nameAndAgeSetV1) EventName() string { return \"person/name_and_age_set_v1\" }\nfunc (*nameAndAgeSetV1) isPersonEvent()    {}\n```\n\nLater, we decide it's better to have separate events for changing the name and age: \n```go\n// V2 events - what our new code uses\ntype nameSetV2 struct {\n    Name string `json:\"name\"`\n}\nfunc (*nameSetV2) EventName() string { return \"person/name_set_v2\" }\nfunc (*nameSetV2) isPersonEvent()    {}\n\ntype ageSetV2 struct {\n    Age int `json:\"age\"`\n}\nfunc (*ageSetV2) EventName() string { return \"person/age_set_v2\" }\nfunc (*ageSetV2) isPersonEvent()    {}\n```\n\nHow do we load an aggregate that has old `nameAndAgeSetV1` events in its history? We create an \"upcaster\" transformer.\n```go\ntype upcasterV1toV2 struct{}\n\nfunc (u *upcasterV1toV2) TransformForRead(\n    _ context.Context,\n    events []PersonEvent,\n) ([]PersonEvent, error) {\n    newEvents := make([]PersonEvent, 0, len(events))\n    for _, e := range events {\n        if oldEvent, ok := e.(*nameAndAgeSetV1); ok {\n            // A V1 event is found, split it into two V2 events\n            newEvents = append(newEvents, \u0026nameSetV2{Name: oldEvent.Name})\n            newEvents = append(newEvents, \u0026ageSetV2{Age: oldEvent.Age})\n        } else {\n            // Not a V1 event, pass it through unchanged\n            newEvents = append(newEvents, e)\n        }\n    }\n    return newEvents, nil\n}\n\n// Write is a pass-through; new code doesn't produce V1 events.\nfunc (u *upcasterV1toV2) TransformForWrite(\n    _ context.Context,\n    events []PersonEvent,\n) ([]PersonEvent, error) {\n    return events, nil\n}\n```\n\nWhen this transformer is added to the repository, here's what happens during a `repo.Get()` call: \n1. The repository reads the raw event records from the event log (e.g., `personWasBorn` at version 1, `nameAndAgeSetV1` at version 2).\n2. The event data is decoded into the Go structs. Your `EventFuncs` must include constructors for both old and new event types so they can be decoded.\n3. The `upcasterV1toV2.TransformForRead` method is called.\n4. It sees the `nameAndAgeSetV1` event and replaces it in memory with a `nameSetV2` and an `ageSetV2` event.\n5. The aggregate's `Apply` method is then called with the transformed list of events. The aggregate's state is built correctly using the new event types.\n\n\u003e [!IMPORTANT]\n\u003e The aggregate's version always reflects the version of the last **persisted** event in the log. Even if an upcaster creates more events in memory, the version number remains consistent with the source of truth. In the example above, the aggregate's final version would be `2`, corresponding to the `nameAndAgeSetV1` event, not `3`.\n\n#### Merging events for compaction\nTransformers can also work in the other direction: merging multiple events into a single, more compact event before writing them to the log. This can be a useful optimization to reduce the number of records for high-frequency events. \n\nFor example, imagine we have a `personAgedOneYear` event that gets recorded frequently. We can create a transformer to batch these up.\n```go\n// Merges multiple personAgedOneYear events on write\nfunc (a *ageBatchingTransformer) TransformForWrite(\n    _ context.Context,\n    events []PersonEvent,\n) ([]PersonEvent, error) {\n    totalYears := 0\n    otherEvents := make([]PersonEvent, 0)\n\n    for _, e := range events {\n        if _, ok := e.(*personAgedOneYear); ok {\n            totalYears++\n        } else {\n            otherEvents = append(otherEvents, e)\n        }\n    }\n\n    if totalYears \u003e 0 {\n        mergedEvent := \u0026multipleYearsAged{Years: totalYears}\n        return append(otherEvents, mergedEvent), nil\n    }\n    return events, nil\n}\n\n// Splits the merged event back up on read\nfunc (a *ageBatchingTransformer) TransformForRead(\n    _ context.Context,\n    events []PersonEvent,\n) ([]PersonEvent, error) {\n    newEvents := make([]PersonEvent, 0, len(events))\n    for _, e := range events {\n        if merged, ok := e.(*multipleYearsAged); ok {\n            for range merged.Years {\n                newEvents = append(newEvents, \u0026personAgedOneYear{})\n            }\n        } else {\n            newEvents = append(newEvents, e)\n        }\n    }\n    return newEvents, nil\n}\n```\n\nWhen you save an aggregate that has recorded five `personAgedOneYear` events, the `TransformForWrite` hook will replace them with a single `multipleYearsAged{Years: 5}` event. This single event is what gets written to the log. \n\nWhen you later load the aggregate, `TransformForRead` does the reverse, ensuring that your aggregate's `Apply` method sees the five individual `personAgedOneYear` events it expects, keeping your domain logic clean and unaware of this persistence optimization. \n\n## Projections\n\nProjections are your read models, optimized for querying. See [the \"what\" section for more info](https://github.com/DeluxeOwl/chronicle?tab=readme-ov-file#what-is-event-sourcing).\n\nThey are **derived** from your event log and can be rebuilt from it (the event log is the source of truth). So in short, projections are and should be treated as disposable.\n\nYou generally want to have many specialized projections, instead of a big read model.\n\nThis framework isn't opinionated in *how* you're creating projections but provides a few primitives that help.\n\n\n### `event.TransactionalEventLog` and `aggregate.TransactionalRepository`\n\nThese are the two interfaces that help us create projections easier.\n\nStarting with `event.TransactionalEventLog`\n```go\npackage event\n\ntype TransactionalEventLog[TX any] interface {\n\tTransactionalLog[TX]\n\tTransactor[TX]\n}\n\ntype Transactor[TX any] interface {\n\tWithinTx(ctx context.Context, fn func(ctx context.Context, tx TX) error) error\n}\n\ntype TransactionalLog[TX any] interface {\n\tAppendInTx(\n\t\tctx context.Context,\n\t\ttx TX,\n\t\tid LogID,\n\t\texpected version.Check,\n\t\tevents RawEvents,\n\t) (version.Version, []*Record, error)\n\tReader\n}\n```\n\nThis is an interface that defines an `Append` method that also provides the `TX` (transactional) type. It's implemented by the following event logs: postgres, sqlite and memory.\n\nAn `aggregate.TransactionalRepository` uses this kind of event log to orchestrate processors.\n\nA processor is called inside an active transaction and provides us access to the root aggregate and to the committed events.\n\n```go\ntype TransactionalAggregateProcessor[TX any, TID ID, E event.Any, R Root[TID, E]] interface {\n\t// Process is called by the TransactionalRepository *inside* an active transaction,\n\t// immediately after the aggregate's events have been successfully saved to the event log.\n\t// It receives the transaction handle, the aggregate in its new state, and the\n\t// strongly-typed events that were just committed.\n\t//\n\t// Returns an error if processing fails. This will cause the entire transaction to be\n\t// rolled back, including the saving of the events. Returns nil on success.\n\tProcess(ctx context.Context, tx TX, root R, events CommittedEvents[E]) error\n}\n```\n\n\n### Example\n\nWe're going to make use of `accountv2`. You can find this example in [examples/6_projections/main.go](./examples/6_projections/main.go) and [examples/internal/accountv2/account_processor.go](./examples/internal/accountv2/account_processor.go).\n\nWe're going to create a simple projection that is very useful in most event sourced application: a table with the account log ids (that also contains the account holder's name).\n\nWe're going to use `sqlite` as the backing event log and as the backing store for our projections.\n\nWhy is it useful? Because it shows us a \"quick view\" of the accounts we have in the system.\n\nIn `accountv2/account_processor.go`:\n```go\ntype AccountsWithNameProcessor struct{}\nfunc (p *AccountsWithNameProcessor) Process(\n\tctx context.Context,\n\ttx *sql.Tx,\n\troot *Account,\n\tevents aggregate.CommittedEvents[AccountEvent],\n) error {\n\t// ...\n\treturn nil\n}\n```\n\nThis is the struct that satisfies our `aggregate.TransactionalAggregateProcessor` interface.\n\nWe're going to use a real time, strongly consistent projection, which is possible because we use `sqlite` and the event log and the projections store is the same - which allows us to update the projection in the same transaction.\n\nBut first, we need a table for our projection, we'll handle it in the constructor for the `AccountsWithNameProcessor` but can be done outside it as well.\n\n```go\nfunc NewAccountsWithNameProcessor(db *sql.DB) (*AccountsWithNameProcessor, error) {\n\t_, err := db.Exec(`\n        CREATE TABLE IF NOT EXISTS projection_accounts (\n            account_id TEXT PRIMARY KEY,\n            holder_name TEXT NOT NULL\n        );\n    `)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"new accounts with name processor: %w\", err)\n\t}\n\n\treturn \u0026AccountsWithNameProcessor{}, nil\n}\n```\n\nNow what's left to do is to implement our processing logic. \n\nWe're only interested in `*accountOpened` events, from which we extract the root id and the `HolderName`:\n\n```go\nfunc (p *AccountsWithNameProcessor) Process(\n\tctx context.Context,\n\ttx *sql.Tx,\n\troot *Account,\n\tevents aggregate.CommittedEvents[AccountEvent],\n) error {\n\tfor evt := range events.All() {\n\t\t// We only care about accountOpened events.\n\t\tif opened, ok := evt.(*accountOpened); ok {\n\t\t\t_, err := tx.ExecContext(ctx, `\n                INSERT INTO projection_accounts (account_id, holder_name) \n                VALUES (?, ?)\n                ON CONFLICT(account_id) DO UPDATE SET \n                    holder_name = excluded.holder_name\n            `, root.ID(), opened.HolderName)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"insert account: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n```\n\nLet's wire it up in `main`:\n```go\nfunc main() {\n\tdb, err := sql.Open(\"sqlite3\", \"file:memdb1?mode=memory\u0026cache=shared\")\n\t// ...\n\tsqlprinter := examplehelper.NewSQLPrinter(db) // We're using a helper to print the tables.\n\n\tsqliteLog, err := eventlog.NewSqlite(db)\n\t// ...\n}\n```\n\nWe can create our processor\n```go\n\taccountProcessor, err := accountv2.NewAccountsWithNameProcessor(db)\n```\n\nAnd hook it up in a `chronicle.NewTransactionalRepository`\n```go\n\taccountRepo, err := chronicle.NewTransactionalRepository(\n\t\t\tsqliteLog,\n\t\t\taccountMaker,\n\t\t\tnil,\n\t\t\taggregate.NewProcessorChain(\n\t\t\t\taccountProcessor,\n\t\t\t), // Our transactional processor.\n\t\t)\n```\n\n`aggregate.NewProcessorChain` is a helper that runs multiple processors one after the other\n\nLet's open an account for Alice and Bob\n```go\n\t// Alice's account\n\taccA, _ := accountv2.Open(accountv2.AccountID(\"alice-account-01\"), timeProvider, \"Alice\")\n\t_ = accA.DepositMoney(100)\n\t_ = accA.DepositMoney(50)\n\t_, _, err = accountRepo.Save(ctx, accA)\n\n\t// Bob's account\n\taccB, _ := accountv2.Open(accountv2.AccountID(\"bob-account-02\"), timeProvider, \"Bob\")\n\t_ = accB.DepositMoney(200)\n\t_, _, err = accountRepo.Save(ctx, accB)\n```\n\nAnd we can use our helper to print the projections table:\n```go\nsqlprinter.Query(\"SELECT account_id, holder_name FROM projection_accounts\")\n\n┌──────────────────┬─────────────┐\n│    ACCOUNT ID    │ HOLDER NAME │\n├──────────────────┼─────────────┤\n│ alice-account-01 │ Alice       │\n│ bob-account-02   │ Bob         │\n└──────────────────┴─────────────┘\n```\n\nPretty nice, huh? We can query the projection table however we like.\n\nWe can also create materialized views in the database by querying the backing sqlite store. Here's an example of querying all events\n```go\nfmt.Println(\"All events:\")\nsqlprinter.Query(\n\t\"SELECT global_version, log_id, version, event_name, json_extract(data, '$') as data FROM chronicle_events\",\n)\n```\n\n**Note:** we're using JSON encoding and we're using `json_extract(data, '$')` to see the data (saved as `BLOB` in sqlite) in a readable format\n\nRunning\n```bash\ngo run examples/6_projections/main.go\n```\n\nPrints\n```bash\nAll events:\n┌────────────────┬──────────────────┬─────────┬─────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐\n│ GLOBAL VERSION │      LOG ID      │ VERSION │       EVENT NAME        │                                                                                            DATA                                                                                             │\n├────────────────┼──────────────────┼─────────┼─────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤\n│ 1              │ alice-account-01 │ 1       │ account/opened          │ {\"eventID\":\"0198f03e-4042-723c-884d-a9e84426eb9e\",\"occuredAt\":\"2025-08-28T13:34:28.29074+03:00\",\"id\":\"alice-account-01\",\"openedAt\":\"2025-08-28T13:34:28.290625+03:00\",\"holderName\":\"Alice\"} │\n│ 2              │ alice-account-01 │ 2       │ account/money_deposited │ {\"eventID\":\"0198f03e-4042-723d-a335-0f13b54c78cb\",\"occuredAt\":\"2025-08-28T13:34:28.290757+03:00\",\"amount\":100}                                                                              │\n│ 3              │ alice-account-01 │ 3       │ account/money_deposited │ {\"eventID\":\"0198f03e-4042-723e-a5e5-b127aec593f2\",\"occuredAt\":\"2025-08-28T13:34:28.290758+03:00\",\"amount\":50}                                                                               │\n│ 4              │ bob-account-02   │ 1       │ account/opened          │ {\"eventID\":\"0198f03e-4043-723e-874c-2095ca72515f\",\"occuredAt\":\"2025-08-28T13:34:28.291034+03:00\",\"id\":\"bob-account-02\",\"openedAt\":\"2025-08-28T13:34:28.291034+03:00\",\"holderName\":\"Bob\"}    │\n│ 5              │ bob-account-02   │ 2       │ account/money_deposited │ {\"eventID\":\"0198f03e-4043-723f-96db-0c01cbf57d70\",\"occuredAt\":\"2025-08-28T13:34:28.291036+03:00\",\"amount\":200}                                                                              │\n└────────────────┴──────────────────┴─────────┴─────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\n```\n\n### Example with outbox\n\n\u003e [!NOTE] \n\u003e For a production environment, take a look at https://watermill.io/advanced/forwarder/ .\n\nWhile strongly-consistent projections (like the one above) are powerful, you often need to notify external systems about events that have occurred. This could involve publishing to a message broker like Kafka/RabbitMQ, calling a third-party webhook, or sending an email. \n\nA common pitfall is the dual-write problem: what happens if you successfully save the events to your database, but the subsequent call to the message broker fails? The system is now in an inconsistent state. The event happened, but the outside world was never notified.\n\nThe Transactional Outbox pattern solves this by leveraging your database's ACID guarantees. The flow is: \n1. Atomically write both the business events (e.g., `accountOpened`) and a corresponding \"message to be sent\" into your database in a single transaction.\n2. A separate, background process polls this \"outbox\" table for new messages.\n3. For each message, it publishes it to the external system (e.g., a message bus).\n4. Once successfully published, it deletes the message from the outbox table.\n\nThis ensures **at-least-once** delivery. If the process crashes after publishing but before deleting, it will simply re-publish the message on the next run. This makes it a reliable way to integrate with external systems.\n\nWe'll build a simple outbox, which will use an in-memory pub/sub channel to act as our message bus.\n\nYou can find this example in [examples/7_outbox/main.go](./examples/7_outbox/main.go) and [examples/internal/accountv2/account_outbox_processor.go](./examples/internal/accountv2/account_outbox_processor.go).\n\nFirst, we create another `TransactionalAggregateProcessor`, the `AccountOutboxProcessor`. Its constructor creates a dedicated outbox table.\n\n```go\nfunc NewAccountOutboxProcessor(db *sql.DB) (*AccountOutboxProcessor, error) {\n\t_, err := db.Exec(`\n        CREATE TABLE IF NOT EXISTS outbox_account_events (\n            id INTEGER PRIMARY KEY AUTOINCREMENT,\n            aggregate_id TEXT NOT NULL,\n            event_name TEXT NOT NULL,\n            payload BLOB NOT NULL\n        );\n    `)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"new account outbox processor: could not create table: %w\", err)\n\t}\n\treturn \u0026AccountOutboxProcessor{}, nil\n}\n```\n\nThe Process logic is simple: it encoded each event to JSON and inserts it into the `outbox_account_events` table using the provided transaction `tx`. This guarantees atomicity with the event log persistence.\n\n```go\n// Process writes committed AccountEvents to the outbox table within the same transaction.\nfunc (p *AccountOutboxProcessor) Process(\n\tctx context.Context,\n\ttx *sql.Tx,\n\troot *Account,\n\tcommittedEvents aggregate.CommittedEvents[AccountEvent],\n) error {\n\tfor _, event := range committedEvents {\n\t\tpayload, err := json.Marshal(event)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"outbox process: failed to marshal event: %w\", err)\n\t\t}\n\n\t\t_, err = tx.ExecContext(ctx, `\n            INSERT INTO outbox_account_events (aggregate_id, event_name, payload) \n            VALUES (?, ?, ?)\n        `, root.ID(), event.EventName(), payload)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"outbox process: insert event: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n```\n\nNow let's wire it up in `main`. We create the processor and add it to our `TransactionalRepository`.\n```go\nfunc main() {\n\tdb, err := sql.Open(\"sqlite3\", \"file:memdb1?mode=memory\u0026cache=shared\")\n\t// ...\n\n\tpubsub := examplehelper.NewPubSubMemory[OutboxMessage]()\n\n\toutboxProcessor, err := accountv2.NewAccountOutboxProcessor(db)\n\t// ...\n\n\tsqliteLog, err := eventlog.NewSqlite(db)\n\t// ...\n\n\trepo, err := chronicle.NewTransactionalRepository(\n\t\tsqliteLog,\n\t\taccountMaker,\n\t\tnil,\n\t\taggregate.NewProcessorChain(outboxProcessor), // The outbox processor\n\t)\n\t// ...\n}\n```\n\nNext, we need the two background components: a **poller** to read from the outbox and a **subscriber** to listen for published messages. For this example, we'll run them as simple goroutines. \n\nThe **subscriber** is easy—it just listens on a channel and prints what it receives: \n```go\n\tgo func() {\n\t\t\tfmt.Println(\"\\nSubscriber started. Waiting for events...\")\n\t\t\tfor msg := range subCh {\n\t\t\t\tfmt.Printf(\n\t\t\t\t\t\"-\u003e Subscriber received: %s for aggregate %s\\n\",\n\t\t\t\t\tmsg.EventName,\n\t\t\t\t\tmsg.AggregateID,\n\t\t\t\t)\n\t\t\t\twg.Done()\n\t\t\t}\n\t\t}()\n```\n\nThe **poller** is a loop that periodically checks the outbox table. In a single transaction, it reads one message, publishes it, and deletes it. \n```go\ngo func() {\n\t\tticker := time.NewTicker(100 * time.Millisecond)\n\t\t\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase \u003c-ctx.Done():\n\t\t\t\treturn\n\t\t\tcase \u003c-ticker.C:\n\t\t\t\t// 1. Begin transaction\n\t\t\t\ttx, err := db.BeginTx(ctx, nil)\n\t\t\t\t// ...\n\n\t\t\t\t// 2. Select the oldest record\n\t\t\t\trow := tx.QueryRowContext(ctx, \"SELECT id, aggregate_id, event_name, payload FROM outbox_account_events ORDER BY id LIMIT 1\")\n\t\t\t\t// ... scan row\n\n\t\t\t\t// 3. Publish the message to the bus\n\t\t\t\tpubsub.Publish(msg)\n\n\t\t\t\t// 4. Delete the record from the outbox\n\t\t\t\t_, err = tx.ExecContext(ctx, \"DELETE FROM outbox_account_events WHERE id = ?\", msg.OutboxID)\n\t\t\t\t// ...\n\n\t\t\t\t// 5. Commit the transaction to atomically mark the event as processed.\n\t\t\t\tif err := tx.Commit(); err != nil {\n\t\t\t\t\tfmt.Printf(\"Error committing outbox transaction: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n```\n\nFinally, we save our aggregates as before. This action triggers the entire flow. \n\n```go\n    // Save Alice's and Bob's accounts\n    accA, _ := accountv2.Open(...)\n    _ = accA.DepositMoney(100)\n    _ = accA.DepositMoney(50)\n    _, _, err = repo.Save(ctx, accA)\n\n    accB, _ := accountv2.Open(...)\n    _ = accB.DepositMoney(200)\n    _, _, err = repo.Save(ctx, accB)\n```\n\nRunning the example shows the whole sequence in action\n```\ngo run examples/7_transactional_outbox/main.go\n\nSaving aggregates... This will write to the outbox table.\n\nSubscriber started. Waiting for events...\nPolling goroutine started.\n\nState of 'outbox_account_events' table immediately after save:\n┌────┬──────────────────┬─────────────────────────┐\n│ ID │   AGGREGATE ID   │       EVENT NAME        │\n├────┼──────────────────┼─────────────────────────┤\n│ 1  │ alice-account-01 │ account/opened          │\n│ 2  │ alice-account-01 │ account/money_deposited │\n│ 3  │ alice-account-01 │ account/money_deposited │\n│ 4  │ bob-account-02   │ account/opened          │\n│ 5  │ bob-account-02   │ account/money_deposited │\n└────┴──────────────────┴─────────────────────────┘\n\nWaiting for subscriber to process all events from the pub/sub...\n-\u003e Subscriber received: account/opened for aggregate alice-account-01\n-\u003e Subscriber received: account/money_deposited for aggregate alice-account-01\n-\u003e Subscriber received: account/money_deposited for aggregate alice-account-01\n-\u003e Subscriber received: account/opened for aggregate bob-account-02\n-\u003e Subscriber received: account/money_deposited for aggregate bob-account-02\n\nAll events processed by subscriber.\n\nOutbox table:\n┌────┬──────────────┬────────────┐\n│ ID │ AGGREGATE ID │ EVENT NAME │\n└────┴──────────────┴────────────┘\nPolling goroutine stopping.\n```\n\nAs you can see, the outbox table was populated atomically when the aggregates were saved. The poller then read each entry, published it, and deleted it, resulting in a reliably processed queue and an empty table at the end. \n\n### Synchronous Projections (`event.SyncProjection`)\n\nA `SyncProjection` is a primitive for building strongly-consistent read models. It operates synchronously, ensuring that the projection is updated within the same database transaction as the events being saved.\n\nThis approach is powerful for use cases that cannot tolerate eventual consistency, such as maintaining unique constraints across aggregates or building a transactional outbox. Unlike the aggregate-specific processors that work with type-safe events, `SyncProjection` operates on generic `event.Record`s. This gives it the flexibility to listen to events from different aggregate types, or even all events in the system.\n\n```go\n// SyncProjection processes events synchronously within the same transaction\n// that appends them. It receives ALL events from a single append operation\n// as a batch, ensuring atomic updates to both the event log and projection.\n//\n// Use cases:\n//   - Transactional outbox pattern\n//   - Denormalized read models requiring strong consistency\n//   - Cross-aggregate invariant enforcement\ntype SyncProjection[TX any] interface {\n\tMatchesEvent(eventName string) bool\n\t// Handle processes a batch of events within a transaction.\n\t// All events are from the same AppendEvents call.\n\tHandle(ctx context.Context, tx TX, records []*Record) error\n}\n```\n\n-   `MatchesEvent(eventName string) bool`: A filter method called for each newly committed event. Your projection should return `true` for event names it wants to process.\n-   `Handle(ctx context.Context, tx TX, records []*Record) error`: The core processing logic. It receives the active transaction handle `tx` and a slice of `*Record`s that matched the filter. You can use the transaction to atomically update your read models. If this method returns an error, the entire transaction (including the event append) is rolled back.\n\nThis interface is orchestrated by `event.TransactableLog`, which wraps a transactional event log and a `SyncProjection` to manage the atomic updates automatically.\n\nUse `event.NewLogWithProjection` or `event.NewTransactableLogWithProjection`. Works with most SQL and KV stores.\n\n#### Example: System Wide Constraints - Unique Usernames\n\nYou can find this example in [examples/8_unique_constraint/main.go](./examples/8_unique_constraint/main.go).\n\nIn event sourcing systems, all state changes are stored as immutable events. However, some business rules require unique constraints (e.g., unique usernames, email addresses, or account numbers) that must be enforced before events are persisted. And eventual consistency models can't guarantee uniqueness at write time.\n\nThis example demonstrates how to implement unique constraints using synchronous projections that execute within the same transaction as event persistence. \n\nThis code uses a synchronous projection that runs in the same database transaction as event insertion: \n```go\ntype uniqueUsernameProjection struct{}\n\nfunc (u *uniqueUsernameProjection) Handle(\n    ctx context.Context,\n    tx *sql.Tx,\n    records []*event.Record,\n) error {\n    // Insert into unique constraint table within same transaction\n    _, err := stmt.ExecContext(ctx, holderName.HolderName)\n    if err != nil {\n        // Constraint violation rolls back entire transaction\n        return fmt.Errorf(\"username '%s' already exists: %w\", holderName.HolderName, err)\n    }\n    return nil\n}\n```\n\nIt uses a dedicated constraint table - basically we moved the constraint check into the db:\n```sql\nCREATE TABLE unique_usernames (\n    username TEXT NOT NULL UNIQUE\n);\n```\n\nRunning the example:\n```bash\n❯ go run examples/8_unique_constraint/main.go\n\nState of unique usernames table:\n┌──────────┐\n│ USERNAME │\n├──────────┤\n│ Alice    │\n└──────────┘\n\nAttempting to create a duplicate user 'Alice'\nSuccessfully prevented duplicate user. Error: repo save: aggregate commit with tx: append events in tx: projection handle records: username 'Alice' already exists: UNIQUE constraint failed: unique_usernames.username\n\nFinal state of unique usernames table:\n┌──────────┐\n│ USERNAME │\n├──────────┤\n│ Alice    │\n└──────────┘\n\nAll events (note that the duplicate 'Alice' event was not saved):\n┌──────────────────┬─────────┬─────────────────────────┬────────────────────────────\n│      LOG ID      │ VERSION │       EVENT NAME        │              DATA                                                                                \n├──────────────────┼─────────┼─────────────────────────┼────────────────────────────\n│ alice-account-01 │ 1       │ account/opened          │ {..., \"holderName\":\"Alice\"} \n│ alice-account-01 │ 2       │ account/money_deposited │ {..., \"amount\":100} \n│ alice-account-01 │ 3       │ account/money_deposited │ {..., \"amount\":50} \n\n```\n\n### Asynchronous Projections (`event.AsyncProjection`)\n\nAn `AsyncProjection` is the primitive for building eventually-consistent read models. It works by processing events from the global, system-wide event stream *after* they have been successfully committed. This is the most common type of projection, ideal for analytics, reporting, search indexes, or notifying external systems where a slight delay is acceptable.\n\nIt operates on generic `event.GlobalRecord`s and is designed for resilience.\n\n```go\n// AsyncProjection processes events asynchronously from the global event stream.\n// It processes one event at a time with explicit checkpoint management for\n// resumability and fault tolerance.\n//\n// Use cases:\n//   - Eventually consistent projections\n//   - Cross-service integration\n//   - Analytics and reporting\n//   - Email notifications, etc.\ntype AsyncProjection interface {\n\tMatchesEvent(eventName string) bool\n\t// Handle processes a single event from the global stream.\n\t// Checkpoint is saved based on the configured CheckpointPolicy.\n\tHandle(ctx context.Context, rec *GlobalRecord) error\n}\n```\n\n-   `MatchesEvent(eventName string) bool`: Filters the global event stream, allowing you to process only the events relevant to your projection.\n-   `Handle(ctx context.Context, rec *GlobalRecord) error`: Called for each individual event that passes the filter. Your logic here is responsible for updating the read model.\n\nAn `AsyncProjection` is driven by the `event.AsyncProjectionRunner`. This runner is a configurable component that manages the entire lifecycle:\n-   It polls or \"tails\" the global event log for new events (for event logs that support tailing)\n-   It passes matching events to your projection's `Handle` method.\n-   It manages checkpoints by saving the `globalVersion` of the last successfully processed event. This ensures that if the projection restarts, it can resume from exactly where it left off, guaranteeing no events are missed.\n\n### Types of projections\n\n\u003e [!WARNING] \n\u003e Wall of text ahead 😴\n\nThere's a lot of projection types, choosing which fits you best requires some consideration\n\n#### By Scope\n\n1. **Per-aggregate projections**\n   * Build the current state of a single entity (aggregate root) by replaying its events.\n   * Example: reconstructing the current balance of one bank account.\n   * ⚠️ In `chronicle`:\n     * This kind of projection is handled by the `repo.Get` method\n\n2. **Global projections**\n   * Span across many aggregates to build a denormalized or system-wide view.\n   * Example: a leaderboard across all players in a game.\n   * ⚠️ In `chronicle`:\n     * This is handled by the `event.GlobalLog` interface, where you can `ReadAllEvents(...)` and by `event.AsyncProjection`\n     * Can also be done by directly querying the backing store of the event log, as seen in [examples/6_projections/main.go](./examples/6_projections/main.go) - where it can be done via sql queries directly.\n\n#### By Behavior\n\n1. **Live / Continuous projections**\n   * Update in near-real-time as new events are appended.\n   * Example: a notification system that reacts to user actions instantly.\n   * ⚠️ In `chronicle`:\n     * If you don't require durability, you can wrap the `Save(...)` method on the repository to publish to a queue\n     * If you require durability, you can use an `*aggregate.TransactionalRepository` to ensure all writes are durable before publishing, or to update the projection directly etc.\n     * Or you can use `event.SyncProjection` with most SQL stores and KV stores.\n\n2. **On-demand / Ad-hoc projections**\n   * Rebuilt only when needed, often discarded afterward.\n   * Example: running an analytics query over a historic event stream.\n   * ⚠️ In `chronicle`:\n     * Can be done by directly querying the backing store or via an `event.GlobalLog`\n\n3. **Catch-up projections**\n   * Rebuild state by replaying past events until caught up with the live stream.\n   * Often used when adding new read models after the system is in production.\n   * ⚠️ In `chronicle`:\n     * Can be done by directly querying the backing store or via an `event.GlobalLog`\n     * It will probably require you to \"remember\" (possibly in a different store) the last processed id, version etc.\n       * This is handled by `event.AsyncProjection`\n     * You might also be interested in idempotency\n\n\n#### By Data Transformation\n\n1. **Aggregated projections**\n   * Summarize events into counts, totals, or other metrics.\n   * Example: total sales per day.\n   * ⚠️ In `chronicle`:\n     * This most definitely requires an `event.GlobalLog` or querying the backing store directly, since you need the events from a group of different aggregates\n     * Can be handled by either an `event.SyncProjection` or `event.AsyncProjection`\n\n2. **Materialized views (denormalized projections)**\n   * Store data in a form that’s ready for querying, often optimized for UI.\n   * Example: user profile with last login, recent purchases, and preferences.\n   * ⚠️ In `chronicle`:\n     * Can be handled by either an `event.SyncProjection` or `event.AsyncProjection`\n\n3. **Policy projections (process managers/sagas)**\n   * React to certain event sequences to drive workflows.\n   * Example: after \"OrderPlaced\" and \"PaymentReceived,\" trigger \"ShipOrder.\"\n   * ⚠️ In `chronicle`:\n     * Can be handled by either an `event.SyncProjection` or `event.AsyncProjection`\n\n#### By Consistency Guarantees\n\n1. **Eventually consistent projections (also called asynchronous projections)**\n   * Most common - projections lag slightly behind the event stream.\n   * ⚠️ In `chronicle`:\n     * This happens if you're using the outbox pattern or have any kind of pub/sub mechanism in your code\n     * Or handled by `event.AsyncProjection`\n\n2. **Strongly consistent projections (also called synchronous projections)**\n   * Rare in event sourcing, but sometimes required for critical counters or invariants. Works really well if the backing store is SQL or KV.\n   * ⚠️ In `chronicle`:\n     * This is done when the backing store is an `event.TransactionalLog` at it exposes the transaction AND you want to store your projections in the same store\n     * Use an `event.SyncProjection`\n     * This is a reasonable and not expensive approach if you're using an SQL store (such as postgres)\n     * It also makes the system easier to work with\n\n#### By How Often You Update\n\n1. **Real-time (push-driven) projections**\n   * Updated immediately as each event arrives.\n   * Common when users expect low-latency updates (e.g., dashboards, notifications).\n   * Mechanism: event handlers/subscribers consume events as they are written.\n\n2. **Near-real-time (micro-batch)**\n   * Updated on a short interval (e.g., every few seconds or minutes).\n   * Useful when strict immediacy isn’t required but throughput matters.\n   * Mechanism: stream processors (like Kafka Streams, Kinesis, or NATS) that batch small sets of events.\n\n3. **Batch/offline projections**\n   * Rebuilt periodically (e.g., nightly jobs).\n   * Useful for reporting, analytics, or expensive transformations.\n   * Mechanism: ETL processes or big data jobs that replay a segment of the event log.\n\n4. **Ad-hoc/on-demand projections**\n   * Rebuilt only when explicitly requested.\n   * Useful when queries are rare or unique (e.g., debugging, exp","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeluxeowl%2Fchronicle","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdeluxeowl%2Fchronicle","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeluxeowl%2Fchronicle/lists"}