Funes
An event sourcing meta-framework designed to provide a frictionless experience for RoR developers to build and operate systems where history is as important as the present. Built with the one-person framework philosophy in mind, it honors the Rails doctrine by providing deep conceptual compression over what is usually a complex architectural pattern.
At its core is a declarative DSL that favors the interpretation of events over all the plumbing. You describe how each event affects state, and Funes handles persistence, ordering, concurrency, and materialization for you.
By distilling the mechanics of event sourcing into just three core concepts — Events, Streams, and Projections — Funes handles the underlying complexity of persistence and state reconstruction for you. It feels like the Rails you already know, giving you the power of a permanent source of truth with the same ease of use as a standard ActiveRecord model.
Unlike traditional event sourcing frameworks that require a total shift in how you build, Funes is designed for progressive adoption. It is a "good neighbor" that coexists seamlessly with your existing ActiveRecord models and standard controllers. You can use Funes for a single mission-critical feature — like a single complex state machine — while keeping the rest of your app in "plain old Rails."
Named after Funes the Memorious, the Borges character who could forget nothing, this framework embodies the principle that in some systems, remembering everything matters.
Installation
Add to your Gemfile:
gem "funes-rails"Run the installation:
$ bin/bundle install
$ bin/rails generate funes:install
$ bin/rails db:migrateCore concepts
Funes bridges the gap between event sourcing theory and the Rails tools you already know (ActiveModel, ActiveRecord, ActiveJob).
-
Events — immutable
ActiveModelobjects that record what happened, with built-in validation and no schema migrations -
Projections — transform a stream of events into a materialized state, either in-memory (
ActiveModel) or persisted (ActiveRecord) -
Event Streams — orchestrate writes, run double validation, and control when projections update (synchronously or via
ActiveJob)
For a full walkthrough of each concept, see the guides.
The DSL
At the heart of Funes is a declarative DSL designed so you spend your time on what matters — interpreting events — not on the plumbing that surrounds them.
A projection reads like a description of your domain logic:
class OutstandingBalanceProjection < Funes::Projection
materialization_model OutstandingBalance
interpretation_for Debt::Issued do |state, event, _at|
state.assign_attributes(outstanding_balance: event.amount)
state
end
interpretation_for Debt::PaymentReceived do |state, event, _at|
state.outstanding_balance -= event.principal_amount
state.last_payment_at = event.at
state
end
endThere is no event-store wiring, no manual replay loop, no serializer configuration. You declare how each event affects state, and Funes takes care of persistence, ordering, concurrency, and materialization. The same DSL scales from a single in-memory validation to a fully persisted read model.
Optimistic concurrency control
Funes uses optimistic concurrency control. Each event in a stream gets an incrementing version number with a unique constraint on (idx, version).
If two processes try to append to the same stream simultaneously, one succeeds and the other gets a validation error — no locks, no blocking.
Three-Tier Consistency Model
Funes gives you fine-grained control over when and how projections run:
| Tier | When it runs | Use case |
|---|---|---|
| Consistency Projection | Before event is persisted | Validate business rules against resulting state |
| Transactional Projections | Same DB transaction as event | Critical read models needing strong consistency |
| Async Projections | Background job (ActiveJob) | Reports, analytics, eventually consistent read models |
Consistency projections
- Guard your invariants: these run before the event is saved to the log. If the resulting state (the "virtual projection") is invalid, the event is rejected and never persisted.
- Business logic validation: This is where you prevent "impossible" states, such as shipping more inventory than is available or overdrawing a bank account.
Transactional projections
-
Atomic updates: these update your persistent read models (
ActiveRecord) within the same database transaction as the event. -
Validation before persistence: before upserting the materialization, Funes runs ActiveRecord validations on the materialization model. If the model is invalid, an
ActiveRecord::RecordInvalidexception is raised, the transaction rolls back, and the event is not persisted. -
Fail-loud on errors: if a projection fails with a database error (e.g., constraint violation) or a validation error, the transaction rolls back, the event is marked as not persisted (
persisted?returnsfalse), and the exception (ActiveRecord::StatementInvalidorActiveRecord::RecordInvalid) propagates. This ensures bugs are immediately visible rather than silently hidden, while keeping the event in a consistent state for any rescue logic in your application.
Async projections
-
Background processing: these are offloaded to
ActiveJob, ensuring that heavy computations don't slow down the write path. -
Native integration: fully compliant with standard Rails job backends (
Sidekiq,Solid Queue, etc.). You can pass standardActiveJoboptions likequeue,wait, orwait_until. -
Temporal control (
temporal_context): customize the temporal reference passed to the projection. The resolved value becomes theatparameter received by interpretation blocks. Note that this is independent from theat:argument ofEventStream#append— that value sets the event'soccurred_at(business time) and does not flow through to async projections.-
:last_event_time(Default): uses the transaction time (created_at) of the last event — i.e., when it was recorded in the database, not when the business event occurred (occurred_at). -
:job_time: uses the current time when the job executes. -
Proc/Lambda: allows for custom temporal logic (e.g., rounding to thebeginning_of_day).
-
Documentation
Guides and full API documentation are available at docs.funes.org.
Compatibility
- Ruby: 3.1, 3.2, 3.3, 3.4
- Rails: 7.1, 7.2, 8.0, 8.1
Rails 8.0+ requires Ruby 3.2 or higher.
License
The gem is available as open source under the terms of the MIT License.