{"id":26716970,"url":"https://github.com/ccamel/erlang-event-sourcing-xp","last_synced_at":"2026-05-09T22:22:39.603Z","repository":{"id":282701493,"uuid":"947783354","full_name":"ccamel/erlang-event-sourcing-xp","owner":"ccamel","description":"🧪 Experimenting with Event Sourcing in Erlang","archived":false,"fork":false,"pushed_at":"2025-03-31T10:06:26.000Z","size":97,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-31T10:38:09.441Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Erlang","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/ccamel.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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}},"created_at":"2025-03-13T08:43:33.000Z","updated_at":"2025-03-31T10:06:28.000Z","dependencies_parsed_at":"2025-06-23T01:39:48.668Z","dependency_job_id":null,"html_url":"https://github.com/ccamel/erlang-event-sourcing-xp","commit_stats":null,"previous_names":["ccamel/erlang-event-sourcing-xp"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/ccamel/erlang-event-sourcing-xp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ccamel%2Ferlang-event-sourcing-xp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ccamel%2Ferlang-event-sourcing-xp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ccamel%2Ferlang-event-sourcing-xp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ccamel%2Ferlang-event-sourcing-xp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ccamel","download_url":"https://codeload.github.com/ccamel/erlang-event-sourcing-xp/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ccamel%2Ferlang-event-sourcing-xp/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261396525,"owners_count":23152450,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":[],"created_at":"2025-03-27T15:50:26.653Z","updated_at":"2026-05-09T22:22:39.593Z","avatar_url":"https://github.com/ccamel.png","language":"Erlang","funding_links":[],"categories":["Erlang"],"sub_categories":[],"readme":"# erlang-event-sourcing-xp\n\n\u003e 🧪 Experimenting with Event Sourcing in Erlang using _pure functional_ principles, [gen_server](https://www.erlang.org/doc/apps/stdlib/gen_server.html)-based aggregates, and _pluggable_ Event Store backends.\n\n[![erlang](https://img.shields.io/badge/Erlang-white.svg?style=for-the-badge\u0026logo=erlang\u0026logoColor=a90533)](https://www.erlang.org/)\n[![lint](https://img.shields.io/github/actions/workflow/status/ccamel/erlang-event-sourcing-xp/lint.yml?label=lint\u0026style=for-the-badge\u0026logo=github)](https://github.com/ccamel/erlang-event-sourcing-xp/actions/workflows/lint.yml)\n[![build](https://img.shields.io/github/actions/workflow/status/ccamel/erlang-event-sourcing-xp/build.yml?label=build\u0026style=for-the-badge\u0026logo=github)](https://github.com/ccamel/erlang-event-sourcing-xp/actions/workflows/build.yml)\n[![test](https://img.shields.io/github/actions/workflow/status/ccamel/erlang-event-sourcing-xp/test.yml?label=test\u0026style=for-the-badge\u0026logo=github)](https://github.com/ccamel/erlang-event-sourcing-xp/actions/workflows/test.yml)\n[![codecov](https://img.shields.io/codecov/c/github/ccamel/erlang-event-sourcing-xp?style=for-the-badge\u0026token=O3FJO5QDCA\u0026logo=codecov)](https://codecov.io/gh/ccamel/erlang-event-sourcing-xp)\n\n[![release](https://img.shields.io/github/release/ccamel/erlang-event-sourcing-xp.svg?style=for-the-badge)](https://github.com/ccamel/erlang-event-sourcing-xp/releases)\n[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=for-the-badge)](https://github.com/semantic-release/semantic-release)\n[![license](https://img.shields.io/badge/license-New%20BSD-blue.svg?style=for-the-badge)](https://github.com/ccamel/erlang-event-sourcing-xp/blob/main/LICENSE)\n\n## About\n\nI'm a big fan of [Erlang/OTP][Erlang] and [Event Sourcing], and I strongly believe that the _Actor Model_ and _Event Sourcing_ are a natural fit. This repository is my way of exploring how these two concepts can work together in practice.\n\nAs an **experiment**, this repo won't cover every facet of event sourcing in depth, but it should provide some insights and spark ideas on the potential of this approach in [Erlang].\n\n[Erlang]: https://www.erlang.org/\n[Event Sourcing]: https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing\n\n## Features\n\n- **Kernel OTP app** — packaged as an OTP application with a supervision tree (`es_kernel_sup`) that boots a dynamic aggregate supervisor and a singleton aggregate manager. Configure stores via application env, start with `application:ensure_all_started/1`, and dispatch commands through `es_kernel:dispatch/1`.\n- **Aggregate** — a reusable [gen_server](https://www.erlang.org/doc/apps/stdlib/gen_server.html) harness that keeps domain logic pure while delegating event sourcing boilerplate.\n- **Aggregate Manager** — a singleton router that spins up aggregates on demand via the dynamic supervisor, rehydrates them from persisted events, monitors them, and passivates idle instances.\n- **Event Store** — a behaviour-driven abstraction with drop-in backends, per-stream replay for aggregates, and global-position replay for read-side projections.\n- **Snapshots** — automatic checkpointing at configurable intervals to avoid replaying entire streams.\n- **Passivation** — idle aggregates are shut down cleanly and will rehydrate from the store on the next command.\n\n## Let's play\n\nThis project is a work in progress, and I welcome any feedback or contributions. If you're interested in [Event Sourcing](https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing), [Erlang/OTP](https://www.erlang.org/), or both, feel free to reach out!\n\nStart the Erlang [shell](https://www.erlang.org/docs/20/man/shell.html) and run the following commands to play with the `es_xp` example application. `es_xp` depends on `es_store_ets` and `es_kernel`, so `application:ensure_all_started(es_xp)` brings up the full stack; commands are dispatched through `es_kernel:dispatch/1`.\n\n\u003c!-- DEMO-START --\u003e\n\n```erlang\n%% Interactive demo showcasing the event sourcing engine.\n%%\n%% Usage:\n%%     rebar3 shell \u003c apps/es_xp/examples/demo_bank.script\n%%\n%% This script configures es_kernel to use the in-memory ETS backend for\n%% both events and snapshots, then starts the es_xp application. es_xp\n%% depends on es_store_ets and es_kernel, so `application:ensure_all_started/1`\n%% brings up the full stack automatically.\n\n%% Explicitly set the store backends (override defaults if needed)\napplication:load(es_kernel),\napplication:set_env(es_kernel, event_store, es_store_ets),\napplication:set_env(es_kernel, snapshot_store, es_store_ets),\n\nio:format(\"~n[1] starting es_xp application (and dependencies)~n\", []),\n{ok, Started} = application:ensure_all_started(es_xp),\nio:format(\" -\u003e ~p~n\", [Started]),\n\nAccountId = \u003c\u003c\"123\"\u003e\u003e,\n\nDispatch = fun(Type, Amount) -\u003e\n    Command =\n        es_contract_command:new(\n            bank_account,\n            Type,\n            AccountId,\n            0,\n            #{},\n            #{amount =\u003e Amount}\n        ),\n    es_kernel:dispatch(Command)\nend,\n\nio:format(\"[2] deposit $100~n\", []),\nio:format(\" -\u003e ~p~n\", [Dispatch(deposit, 100)]),\n\nio:format(\"[3] withdraw $10~n\", []),\nio:format(\" -\u003e ~p~n\", [Dispatch(withdraw, 10)]),\n\nio:format(\"[4] withdraw $1000 (should fail)~n\", []),\nio:format(\" -\u003e ~p~n\", [Dispatch(withdraw, 1000)]),\n\ntimer:sleep(500),\n\nok.\n```\n\u003c!-- DEMO-END --\u003e\n\n## Architecture\n\n### Overview\n\nThis project is structured around the core principles of Event Sourcing:\n\n- All changes are represented as immutable events.\n- Aggregates handle commands and apply events to evolve their state.\n- State is rehydrated by replaying historical events. Possible optimizations include snapshots and caching.\n\n### Kernel application \u0026 supervision\n\n- `es_kernel` ships as an OTP application. Start it with `application:ensure_all_started(es_kernel)` after configuring `event_store` and `snapshot_store` (defaults to `es_store_ets`).\n- Start the configured store backends first (e.g., `es_store_ets:start/0`) so tables/processes exist before aggregates boot. The `es_xp` example app handles this by depending on `es_store_ets` and `es_kernel` and starting them via `application:ensure_all_started(es_xp)`.\n- The top-level supervisor (`es_kernel_sup`) starts:\n  - `es_kernel_aggregate_sup`: a dynamic supervisor that spawns `es_kernel_aggregate` processes on demand.\n  - `es_kernel_mgr_aggregate`: a registered singleton that routes commands to aggregates and keeps track of live PIDs.\n- Commands are dispatched through the public API `es_kernel:dispatch/1`, which forwards to the registered manager.\n\n### Store abstraction\n\nThe store layer separates domain logic from persistence concerns through two behaviour contracts: `es_contract_event_store` for event persistence and `es_contract_snapshot_store` for snapshot optimization. Backends implement these behaviours, and aggregates interact with stores through the unified `es_kernel_store` API.\n\n#### Event Store\n\nThe event store is the heart of event sourcing persistence, designed as a `behaviour` (`es_contract_event_store`) that backends implement. It guarantees **ordering** (events replayed in sequence or global position order), **atomicity** (all-or-nothing persistence), and **immutability** (events never modified).\n\nEvents carry a stream-local `sequence`, used to rebuild a single aggregate. Store backends also assign a monotonically increasing global `position` when events are persisted. This position is storage metadata, not part of the domain event map, and is used for global replay, projections, and future read-side subscriptions.\n\n**Required Callbacks:**\n\n```erlang\n% Appends events to a stream with monotonic sequence ordering\n-callback append(StreamId, Events) -\u003e ok | {error, Reason}\n    when StreamId :: es_contract_event:stream_id(),\n         Events :: [es_contract_event:t()],\n         Reason :: term().\n\n% Replays events for one stream in sequence order\n-callback fold(StreamId, FoldFun, Acc0, Range) -\u003e\n    {ok, Acc1} | {error, Reason}\n    when StreamId :: es_contract_event:stream_id(),\n         FoldFun :: fun(\n             (Event :: es_contract_event:t(),\n              Sequence :: es_contract_event:sequence(),\n              AccIn) -\u003e AccOut\n         ),\n         Acc0 :: term(),\n         Range :: es_contract_range:range(),\n         Acc1 :: term(),\n         AccIn :: term(),\n         AccOut :: term(),\n         Reason :: term().\n\n% Replays all events across streams in global position order\n-callback fold_all(FoldFun, Acc0, Range) -\u003e\n    {ok, Acc1} | {error, Reason}\n    when FoldFun :: fun(\n             (Event :: es_contract_event:t(),\n              Position :: es_contract_event_store:position(),\n              AccIn) -\u003e AccOut\n         ),\n         Acc0 :: term(),\n         Range :: es_contract_range:range(),\n         Acc1 :: term(),\n         AccIn :: term(),\n         AccOut :: term(),\n         Reason :: term().\n```\n\n`fold/4` is the aggregate replay primitive: it reads one stream using stream-local sequence ranges. `fold_all/3` is the read-side primitive: it reads the global event log using position ranges. The kernel wrapper also exposes `es_kernel_store:fold_all/4` when callers need to provide an explicit accumulator.\n\n#### Projections\n\n`es_contract_projection` defines the read-side projection behaviour:\n\n```erlang\n-callback init() -\u003e projection_state().\n-callback name() -\u003e atom().\n-callback handle_event(Event, State) -\u003e {ok, NewState} | {error, Reason}.\n-callback event_filter(Event) -\u003e boolean().\n```\n\n`event_filter/1` is optional. If omitted, the projection is interested in all events. Projection modules describe how to transform events into read-side state; they do not decide how events are consumed from the store.\n\n`es_projection` provides a pull-based projection runtime on top of the global event log:\n\n```erlang\n% Run catch-up once and return the final projection state\nes_projection:run_once(StoreContext, ProjectionModule, Options).\n\n% Start a polling projection runner\nes_projection:start_link(StoreContext, ProjectionModule, Options).\n\n% Start a managed polling projection runner under es_projection_sup\nes_projection:start(StoreContext, ProjectionModule, Options).\n\n% Locate or stop a managed projection by ProjectionModule:name/0\nes_projection:lookup(ProjectionName).\nes_projection:stop(ProjectionName).\n```\n\nThe runner owns checkpointing. It loads the last processed global position for `ProjectionModule:name/0`, consumes events with `es_kernel_store:fold_all/4`, applies `event_filter/1` when present, calls `handle_event/2`, and stores the checkpoint after each processed position. Filtered events are checkpointed too, so a projection can keep moving through the global log.\n\nBy default, checkpoints are stored in ETS through `es_projection_checkpoint_ets`. Callers can provide another checkpoint backend with the `checkpoint_store` option. `start_position` defaults to `0`, and `poll_interval` defaults to `200` milliseconds.\n\nThe polling runner is fail-fast: any store, checkpoint, or projection handling error stops the process. `es_projection:start/3` starts runners under the projection dynamic supervisor and tracks them by projection name through the manager. `start_link/3` remains available for callers that want to own the runner process directly.\n\n#### Snapshot Store\n\nSnapshots provide optional **performance optimization** by checkpointing aggregate state, avoiding full event replay from stream start. They remain secondary to events, which are always the source of truth.\n\n**Required Callbacks:**\n\n```erlang\n% Persists a snapshot; returns warning instead of crashing on failure\n-callback store(Snapshot) -\u003e ok | {warning, Reason}\n    when Snapshot :: es_contract_snapshot:t(),\n         Reason :: term().\n\n% Retrieves the most recent snapshot for a stream\n-callback load_latest(StreamId) -\u003e {ok, Snapshot} | {error, not_found}\n    when StreamId :: es_contract_snapshot:stream_id(),\n         Snapshot :: es_contract_snapshot:t().\n```\n\n**How Snapshots Work:**\n\n1. **On aggregate startup**: `load_latest/1` fetches the most recent snapshot (if available)\n2. **Replay optimization**: Only events _after_ the snapshot sequence are replayed via `fold/4`\n3. **Automatic checkpointing**: When `snapshot_interval` is configured (e.g., `10`), snapshots save at sequence multiples (10, 20, 30...)\n\n**Configuring Snapshots:**\n\nSet `snapshot_interval` per aggregate or globally via `es_kernel` application environment:\n\n```erlang\n% Global default in sys.config\n{es_kernel, [{snapshot_interval, 10}]}\n\n% Or programmatically\napplication:set_env(es_kernel, snapshot_interval, 10)\n```\n\nSetting `snapshot_interval =\u003e 0` (default) disables automatic snapshotting.\n\n#### Additional future features\n\n- Add durable projection checkpoint backends for file or Mnesia stores if needed.\n- Support optional event subscriptions for real-time read-side updates.\n- Implement snapshot retention policies (e.g., keep only last N snapshots).\n\n#### Backend roadmap\n\n| Backend                                                                     | Status     | Icon                                                                                                                               | Capabilities | Highlights                                                                                     | Ideal use cases                                                                                   |\n| --------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |\n| [ETS](https://www.erlang.org/doc/apps/stdlib/ets.html)       | ✅ Ready   | \u003cimg height=\"50\" src=\"https://raw.githubusercontent.com/marwin1991/profile-technology-icons/refs/heads/main/icons/erlang.png\" alt=\"ets-logo\"\u003e     | Events + snapshots | In-memory tables backed by the BEAM VM, blazing-fast reads/writes, zero external dependencies. | Local development, benchmarks, ephemeral environments where latency matters more than durability. |\n| File (pedagogical)                                                          | ✅ Ready   | 📁                                                                                                                                  | Events + snapshots | Plain files (one Erlang term per line) under a configurable root dir, zero dependencies.        | Learning/teaching runs where you want to peek at persisted state without external services.       |\n| [Mnesia](https://www.erlang.org/docs/29/apps/mnesia/mnesia.html) | ✅ Ready   | \u003cimg height=\"50\" src=\"https://raw.githubusercontent.com/marwin1991/profile-technology-icons/refs/heads/main/icons/erlang.png\" alt=\"mnesia-logo\"\u003e     | Events + snapshots | Distributed, transactional, and replicated storage built into Erlang/OTP.                      | Clusters that need lightweight distribution without introducing an external database.             |\n| [PostgreSQL](https://www.postgresql.org/)                                   | 🛠️ Planned | \u003cimg height=\"50\" src=\"https://raw.githubusercontent.com/marwin1991/profile-technology-icons/refs/heads/main/icons/postgresql.png\" alt=\"postgresql-logo\"\u003e | Events + snapshots | Durable SQL store with strong transactional guarantees and easy horizontal scaling.            | Production setups that already rely on Postgres or need rock-solid consistency.                   |\n| [MongoDB](https://www.mongodb.com/)                                         | 🛠️ Planned | \u003cimg height=\"50\" src=\"https://raw.githubusercontent.com/marwin1991/profile-technology-icons/refs/heads/main/icons/mongodb.png\" alt=\"mongodb-logo\"\u003e    | Events + snapshots | Flexible document database with built-in replication and sharding.                             | Event streams that benefit from schemaless payload storage or multi-region clusters.              |\n\n### Aggregate\n\nThe _aggregate_ is implemented as a [gen_server](https://www.erlang.org/doc/apps/stdlib/gen_server.html) that encapsulates _domain logic_ and delegates event persistence to a pluggable Event Store (e.g. [ETS](https://www.erlang.org/doc/apps/stdlib/ets.html) or [Mnesia](https://www.erlang.org/doc/apps/mnesia/mnesia.html)).\n\nThe core idea is to separate concerns between domain behavior and infrastructure. To achieve this, the system is structured into three main components:\n\n- 🧩 **Domain Module** — a pure module that implements domain-specific logic via _behaviour_ callbacks.\n- ⚙️ **`aggregate`** — the glue that bridges domain logic and infrastructure (event sourcing logic, event persistence, etc.).\n- 🚦 [`gen_server`](https://www.erlang.org/doc/apps/stdlib/gen_server.html) — the OTP mechanism that provides lifecycle management and message orchestration.\n\nThe `aggregate` provides:\n\n- A [behaviour](https://www.erlang.org/doc/system/design_principles.html#behaviours) for domain-specific modules to implement.\n- A generic [OTP](https://www.erlang.org/doc/system/design_principles.html) [gen_server](https://www.erlang.org/doc/apps/stdlib/gen_server.html) that:\n  - Rehydrates state from events on startup (with optional snapshot loading).\n  - Processes commands to produce events.\n  - Applies events to evolve internal state.\n  - Automatically passivates (shuts down) after inactivity.\n  - Saves snapshots at configurable intervals for optimization.\n\nThe following diagram shows how the system processes a command using the event-sourced aggregate infrastructure.\n\n```mermaid\nsequenceDiagram\n    actor User\n    participant Kernel as es_kernel (API)\n    participant AggMgr as es_kernel_mgr_aggregate\n    participant AggSup as es_kernel_aggregate_sup\n    participant Agg as es_kernel_aggregate\n    participant DomainModule as AggregateModule (callback)\n\n    User -\u003e\u003e Kernel: es_kernel:dispatch(Command)\n    Kernel -\u003e\u003e AggMgr: gen_server:call(?MODULE, Command)\n\n    alt aggregate not running\n        AggMgr -\u003e\u003e AggSup: start_aggregate(Module, Store, Id, Opts)\n        AggSup --\u003e\u003e AggMgr: {ok, Pid}\n    end\n\n    AggMgr -\u003e\u003e Agg: es_kernel_aggregate:execute(Pid, Command)\n    Agg -\u003e\u003e DomainModule: handle_command(Command, State)\n    Agg -\u003e\u003e Agg: persist_events(Store, Events)\n\n    loop For each Event\n        Agg -\u003e\u003e DomainModule: apply_event(Event, State)\n    end\n```\n\n#### Passivation\n\nEach aggregate instance (a `gen_server`) is automatically passivated — i.e., stopped — after a period of inactivity.\n\nThis helps:\n\n- Free up memory in long-lived systems\n- Keep the number of live processes bounded\n- Rehydrate state on demand from the event store\n\nPassivation is configured via a `timeout` value when the aggregate is started (defaults to 5000 ms):\n\n```erlang\nes_kernel_aggregate:start_link(Module, Store, Id, #{timeout =\u003e 10000}).\n```\n\nWhen no messages are received within the timeout window:\n\n- A passivate message is sent to the process.\n- The aggregate process exits normally (`stop`).\n- Its state is discarded.\n- Future commands will cause the manager to rehydrate it from persisted events.\n\n#### Snapshots\n\nSnapshots provide a performance optimization for aggregate rehydration by avoiding the need to replay all events from the beginning of a stream.\n\n**How it works:**\n\n1. **On startup**, the aggregate:\n\n   - Attempts to load the latest snapshot from the event store\n   - If found, initializes state from the snapshot\n   - Replays only events that occurred after the snapshot sequence\n\n2. **During command processing**, snapshots are automatically created when:\n   - A `snapshot_interval` is configured (e.g., `10`)\n   - The current sequence number is a multiple of the interval\n   - For example, with `snapshot_interval =\u003e 10`, snapshots are saved at sequences 10, 20, 30, etc.\n\n**Configuration:**\n\n```erlang\n% Create aggregate with snapshots every 10 events\nes_kernel_aggregate:start_link(\n    bank_account_aggregate,\n    es_store_ets,\n    \u003c\u003c\"account-123\"\u003e\u003e,\n    #{\n        timeout =\u003e 5000,\n        snapshot_interval =\u003e 10  % Save snapshot every 10 events\n    }\n).\n```\n\nSetting `snapshot_interval =\u003e 0` (the default) disables automatic snapshotting.\n\n### Aggregate Manager\n\nThe _aggregate manager_ is a [gen_server](https://www.erlang.org/doc/apps/stdlib/gen_server.html) started by `es_kernel_sup` and registered as `es_kernel_mgr_aggregate`. It routes `es_contract_command:t()` to the right aggregate process, spins up instances via the dynamic supervisor, and monitors them to keep its registry clean. The store context is taken from the `es_kernel` application environment, and the public entrypoint `es_kernel:dispatch/1` forwards to this singleton.\n\nThe manager is responsible for:\n\n- Routing commands to the appropriate aggregate process across all aggregate types.\n- Starting aggregate instances on demand through `es_kernel_aggregate_sup`.\n- Monitoring aggregate processes (for passivation or crashes) and cleaning up registry entries when they terminate.\n\n#### How it works\n\nThe aggregate manager maintains a mapping of `{AggregateType, AggregateId}` to aggregate process PIDs. When a command is received (typically via `es_kernel:dispatch/1`):\n\n1. It extracts the `aggregate_type` and `aggregate_id` from the command map.\n2. The `aggregate_type` is resolved to its implementing module via `es_kernel_registry`.\n3. The internal `pids` map is checked for an existing aggregate instance.\n4. If none exists, the manager asks `es_kernel_aggregate_sup` to start the aggregate, monitors the new PID, and stores it in the registry.\n5. The command is forwarded to the aggregate via `es_kernel_aggregate:execute/2`.\n6. When an aggregate terminates (passivation or crash), the monitor `'DOWN'` message removes it from the registry so a new command will recreate it if needed.\n\n```mermaid\nflowchart LR\n    %% Supervision tree\n    Kernel((es_kernel\u003cbr\u003eapplication)):::app\n    KernelSup([es_kernel_sup]):::supervisor\n    AggSup([es_kernel_aggregate_sup]):::supervisor\n    AggMgr([es_kernel_mgr_aggregate\u003cbr\u003esingleton]):::manager\n\n    %% Aggregate Instances\n    AggOrder((order\u003cbr\u003eorder-123)):::aggregate\n    AggUser((user\u003cbr\u003euser-456)):::aggregate\n\n    Kernel --\u003e KernelSup\n    KernelSup -.-\u003e|supervises| AggSup\n    KernelSup -.-\u003e|supervises| AggMgr\n    AggSup -.-\u003e|starts| AggOrder\n    AggSup -.-\u003e|starts| AggUser\n    AggMgr --\u003e|cmd| AggOrder\n    AggMgr --\u003e|cmd| AggUser\n    AggMgr -.-\u003e|monitor| AggOrder\n    AggMgr -.-\u003e|monitor| AggUser\n\n    AggOrder --\u003e|apply\u003cbr\u003eevents| AggOrder\n    AggUser --\u003e|apply\u003cbr\u003eevents| AggUser\n\n    classDef supervisor stroke-dasharray: 5 5,stroke-width:2;\n    classDef manager stroke:#5b4ae0,stroke-width:2;\n    classDef aggregate stroke:#222,stroke-width:1.5;\n    classDef app stroke:#0b67a3,stroke-width:2;\n```\n\n#### Options\n\nThe manager options are applied to the aggregates it starts:\n\n- `timeout`: Inactivity timeout passed to aggregates.\n- `now_fun`: Function to provide timestamps for events/snapshots.\n\n## Build\n\n```sh\nrebar3 compile\n```\n\n## Test\n\n```sh\nrebar3 eunit\n```\n\n## Lint\n\n```sh\nrebar3 do dialyzer, fmt --check\n```\n\n`dialyzer` runs the type analysis, while `fmt --check` makes sure all Erlang sources are already formatted.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fccamel%2Ferlang-event-sourcing-xp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fccamel%2Ferlang-event-sourcing-xp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fccamel%2Ferlang-event-sourcing-xp/lists"}