Event Sourcing and materialization in Elixir
Series: Part 4 of 5 — CQRS and architecture for AI agents.
Reading time: ~6 min.
In the previous article we covered Ask vs Act (RAG and Tool Use) for AI agents. Here we cover Event Sourcing as a timeline and materialization in Elixir with Commanded and Projectors.
Event-driven architecture: persistence as a timeline
The CQRS pattern reaches its full architectural potential when combined with Event Sourcing (ES). Instead of storing the current state of an object in the database and overwriting previous records with destructive UPDATEs, Event Sourcing holds that every change in the system should be recorded as an immutable domain event in an append-only log.
The current state of a system does not exist as a single consolidated entity in the transactional store; it is derived by replaying all past events in sequence. Example timeline: GroupCreated → MemberAdded → WalletFunded → WalletLiquidated. The group’s current state is the sequential application of those events.
The synergy between CQRS and Event Sourcing provides vital capabilities for modernizing enterprise domains and AI orchestration:
Cryptographic auditability and time travel: Because events are immutable, the system keeps a perfect timeline of every operational decision. In the context of AI governance this provides an indisputable forensic record of "why" an agent made a decision and "when" the context changed — something CRUD does not offer.
Fully asynchronous optimization: Persisting the intention (appending the event) is very fast. In parallel, the Event Store reliably publishes the event to a fleet of Projectors. These listeners consume the event log and asynchronously update the various CQRS read models, feeding MongoDB for reports, PostgreSQL tables for dashboards, and Pinecone for semantic search.
Organic concurrency resolution: Event Sourcing avoids the pitfalls of pessimistic database locks by using optimistic locking focused on aggregate versioning. Multiple AI agents can submit commands at once. The event store checks the logical version at which the intention was conceived; only mutations that do not violate state invariants proceed, while stale commands are aborted early, removing lock contention at the infrastructure layer.
Technological materialization in Elixir: the BEAM ecosystem serving CQRS
Looking at the pattern in isolation without its technical embodiment misses important engineering nuance. The Elixir environment, built on the Erlang VM (BEAM), is a strong fit for designing CQRS and domain-event applications, providing concurrency through the Actor model.
The open-source library Commanded makes it possible to orchestrate complex CQRS/ES systems in Elixir, encapsulating routing and long-running process handling (Process Managers and Sagas).
When modeling a business subdomain in Elixir (such as the logic grouped in Trips4you.Finance), the physical separation is encoded by strictly applying the Facade pattern.
Anatomy of execution: the business facade
defmodule Trips4you.Finance.Commands do
@moduledoc "Entry point for Commands that change reality."
def liquidate_group_with_wallets(scope, group_id, opts \\ []) do
Commanded.Commands.Router.dispatch(%LiquidateGroupWithWallets{
scope: scope,
group_id: group_id,
opts: opts
})
end
end
In this model, the LiquidateGroupWithWallets command is the imperative structure. The Commanded.Commands.Router receives that intention, finds the correct aggregate from the event identifier and runs the associated state machine. Inside the Aggregate, pure Elixir functions (execute/2) assert the business validity of the operation and reject when invariants are violated. If accepted, one or more events are emitted (e.g. %GroupLiquidated{}).
The architecture also supports variants such as AshCommanded, which combines the declarative DSL of the Ash framework with Commanded’s robust backbone, so developers can build CQRS/ES machinery with less manual boilerplate.
Read projections and multimodal integration in Elixir
While the execution layer processes immutable data, Elixir Projectors build the read instances (the Query side). With native integration to libraries like Ecto, events can update traditional relational databases asynchronously. Elixir’s integration strength also shows in feeding data to AI agents.
Tools like Bumblebee — an ecosystem that connects Hugging Face pre-trained models with the BEAM’s JIT tensor compilation (via Nx and EXLA backends) — make it much easier to generate semantic embeddings at event ingestion time.
When a document is added via a Command, the DocumentAdded event is published. A projector in the background listens for that event, runs the Bumblebee pipeline (e.g. an optimized Bumblebee.Text.TextEmbedding server), turns the text into vectors and syncs the Read Model by persisting the enriched content into pgvector via the Ecto-supported extension (Pgvector.Ecto.Vector).
This whole cycle runs fully isolated from the command core, so that heavy AI processing does not create bottlenecks for the platform’s essential operational responses.
Enjoyed it? Support the author with a coffee.
Previous: Ask vs Act: RAG, Tool Use and AI agents
Next: Evolution without Big Bang: Hexagonal, Strangler and governance
Top comments (0)