event sourcing · ruby on rails

Event-sourcing as a first-class citizen in Rails applications.

Funes is an event sourcing meta-framework for Rails, providing deep conceptual compression over a notoriously complex pattern. Built for the "one person framework" philosophy and designed for progressive adoption.

Three new concepts. One concise mental model.

These three concepts are the conceptual compression of an event-sourced system, a synthesis of its mechanics and operation. Built on top of Rails components, they live well with the Rails way of doing things, which is why anyone fluent in Rails picks them up quickly.

01. Event

An immutable registry of a fact. Validated on the way in, every event composes the permanent history, the raw material from which your application's state is derived.

Read more

02. Stream

The write interface for one entity, and the way to aggregate related events in the event log. Appends are validated, concurrent writes are managed, and a new stream is born the first time you write to it. No provisioning, no setup.

Read more

03. Projection

Turns the immutable log into live state your app can use. Virtual for in-memory validation, persisted for fast reads. Funes handles replay, ordering, and concurrency for you.

Read more

Together, the three concepts give you perspective on the events accumulating in your log. And the interpretations inside each projection let you pilot a time machine: rewind to see how state actually was, or play forward to see how it could be if the domain unfolds the way you modeled it. That's the mental model: a record of facts, any of many possible interpretations, and time as a dial you can spin in either direction, with the resulting state either virtual in memory or persisted for fast reads.

Time becomes a dimension you can shape.

In event-sourced systems, time stops being a side-effect and becomes a dimension you can model directly. Two clocks tick in parallel: one for the business reality (when something occurred), another for the system's awareness of it (when the event was recorded). Funes keeps both, so the past stays auditable, and the future is something you can actually project.

Reconstruct any past moment.
Pin the dial to a date and read the state as it appeared then: what someone saw on a screen, what an auditor would have seen at month-close, what the state was before the correction landed.
Correct without erasing.
When the world (or the system's knowledge of it) shifts, retroactive events update the past without overwriting it. Both versions stay on the record, and so does the moment the correction was made. The bi-temporal event streams recipe covers the pattern in detail.
Project the future.
The same interpretation runs against speculative timelines: interest that hasn't accrued, contracts that haven't matured, scenarios that haven't happened. Your domain model decides what the future looks like.

Interpret events. Skip the plumbing.

Event-sourced systems are tricky to shape on their own. The plumbing around them (persistence, ordering, concurrency, materialization wiring) doesn't make it any easier. The DSL at the heart of Funes hides that machinery so you spend your time on what actually matters: interpreting events in terms of your business logic.

app/projections/simple_interest_debt_projection.rb
class SimpleInterestDebtProjection < Funes::Projection
  materialization_model DebtVirtualSnapshot

  interpretation_for Debt::Issued do |state, event, at|
    state.assign_attributes(principal: event.amount, daily_interest: event.annual_rate / 365.0,
                            contract_date: at)
    state
  end

  final_state do |state, at|
    elapsed_days = (at - state.contract_date) / 1.day
    state.present_value = state.principal + (state.principal * state.daily_interest * elapsed_days)
    state
  end
end

As an entire event stream is interpreted, the DSL handles the pattern matching for you. Each event finds its interpretation_for block; Funes dispatches the rest.

Notice the at threading through every interpretation. Funes hands the temporal reference to your code, so time-aware derivations — interest, vesting, depreciation — become a simpler operation.

Here's the delightful part: testing follows the exact same declarative shape as the projection itself. Pick a moment in time, project, assert:


Full testing helpers and conventions live in the testing documentation.

test/projections/simple_interest_debt_projection_test.rb
# Given a debt issued with principal 1_000 and annual_rate 0.10.

test "on the issuance day, present_value equals the principal" do
  result = final_state(given: @state_after_issuance, at: @contract_date)
  assert_equal 1_000, result.present_value
end

test "after one year, present_value reflects one year of accrued interest" do
  result = final_state(given: @state_after_issuance,
                       at: @contract_date + 1.year)
  assert_equal 1_100, result.present_value
end

Leading Rails into less-explored territory.

Rails is already a big lever for building products. But for the kinds of systems where event sourcing is battle-tested and an industry standard, Rails alone runs out of abstractions before the problem is solved. Building event-sourced systems on top of Rails has never been impossible, just never quite easy. Funes' architecture is intentionally flexible and creative enough to apply across many contexts, but it thrives in exactly the kinds of domains where Rails has quiet success stories the community rarely talks about.

More than a status. The whole memory.

The system can return to any past moment and replay any state used as input to a decision — same events in, same outcome out.

  • State decisions and accumulated metrics both read off the full history: "escalate after three failed AI retries", total tokens spent, who resolved each intervention — all derived from the same events, no flags or counters to maintain.
  • LLM context rebuilt as a projection of the event stream: stateless by design, so what the model saw is reproducible without a separate prompt log.

Money has a story. And a future to prospect.

Deposits, debts, payments — financial primitives are business concepts that fit naturally into an event-sourced model. Funes shapes around them, so every cent traces back to the act that produced it.

  • Temporal queries — "what was the balance on December 1st?" and "what will it be on February 15th?"
  • Correctness by construction: no race conditions double-withdrawing your deposits, and consistency projections that block invalid states before they exist.

Ledger entries as a side-effect of your events.

When economic events are first-class citizens of the system (Funes events), ledgers stop being a separate concern and become a projection — one persistent projection among many, rebuildable from the same source of truth.

  • Derive double-entry ledgers from domain events
  • Rebuild from history when the chart of accounts changes — and if the chart of accounts is itself an entity with its own time machine, every past entry stays consistent with the rules that were in force when it happened.

Frequently asked questions

Will it stay fast as the event log grows?

Yes — measurements consistently show that event-stream operations stay sub-linear as the log grows, which is an important property for medium- and long-sized streams. Treat any published figure as directional rather than definitive (workloads vary, hardware matters), but the overall trend stays the same. Latest measurements and methodology live in the performance measurements discussions.

That said, streams cannot be undefined in size. Model the system so that every stream has a sensible, bounded lifetime — a stream per order, per account period, per session — rather than a single ever-growing stream that accumulates forever. Good stream boundaries are part of the design, not an afterthought.

Is Funes responsible for infrastructure compliance?

No. Funes is an application-layer framework: it bridges Rails and event sourcing, and nothing below that. Everything at the infrastructure layer (encryption, SOC 2 controls, access governance) stays the responsibility of the developer or operator. The event log lives in your database, in your environment, configured to whatever your organization requires.

One friendly request, though: please don't persist PII inside an event. Events are immutable by design — once written, they stay written — so any personal data you drop into a payload is effectively there forever. Keep PII in mutable side tables you can redact or rotate, and reference it from events by stable identifiers.

Why not use one of the existing event-sourcing options on the market?

Fair question — there are excellent tools out there, and we have a lot of respect for them. Funes isn't trying to out-feature any of them. The bet is the opposite one: most existing options reach for broader concepts (CQRS, sagas, process managers, full DDD tactical patterns, dedicated event stores, message buses) that are useful in their own right but sit above and around event sourcing rather than inside it.

Funes deliberately compresses the surface area down to three primitives — events, streams, projections — wrapped in a small, precise DSL. The goal is to remove complexity rather than add more of it: keep the core idea sharp, keep the noise out, and let teams pick up event sourcing without first adopting a wider architectural vocabulary. If you already love the bigger toolkits, they remain great choices; Funes is for teams who want the essence of event sourcing inside a familiar Rails app.

Can I adopt Funes without rewriting my app?

No. Funes is designed for progressive adoption — it's a "good neighbor" that coexists with your existing ActiveRecord models and standard controllers. You can use it for a single mission-critical feature while keeping the rest of your app in plain old Rails. Nothing forces an event-source-everything commitment across the codebase.

Why "meta-framework" and not a Rails engine?

Technically, Funes is a Rails engine: it ships as a gem and mounts into your application like any other. The "meta-framework" label isn't about packaging, it's about vocabulary. A typical engine extends an application with features that fit cleanly into the lingo developers already share. Funes goes a step further: it introduces a small set of new concepts (events, streams, projections, interpretations) that aren't part of everyday Rails vocabulary. That extra vocabulary built on Rails, rather than just inside it, is what makes "meta-framework" a better fit than "engine."

How does Funes fit in an AI-coding era?

AI is far better at gluing well-defined building blocks together than at deriving an entire architecture from first principles. As the cost of writing code falls, the leverage shifts to whoever ships clean, composable primitives that an assistant (or an engineer) can stitch into a system without re-inventing the fundamentals every time. Funes is shaped for that posture: three concepts, a focused DSL, and a clear contract for events, streams, and projections. Mitchell Hashimoto calls this dynamic the building-block economy, and a well-defined event-sourcing framework is precisely the kind of block it rewards.

Who makes Funes?

Funes is architected and maintained by Vinícius Almeida, drawing on his experience building financial systems and complex auditable service operations. It's developed in the open at github.com/funes-org under the MIT license.

What's behind the name Funes?

Named after Funes the Memorious — the Borges short story about a young man who, after a fall from a horse, becomes incapable of forgetting. Every detail of every moment lodges in his mind in perfect resolution. The framework borrows that conceit: in some systems, the past matters as much as the present, and remembering everything is the point.