Virtual projections
Table of contents
- Define the projection’s materialization model
- Wire the materialization model to the projection
- When to use it
Funes computes a virtual materialization model on the fly and writes nothing anywhere. That makes virtual projections a natural fit for consistency checks (they validate an event’s effect on state before it lands) and for ad-hoc queries where you don’t need a queryable read table.
Define the projection’s materialization model
Materialization models for virtual projections are plain ActiveModel classes. ActiveModel is a well-established Rails convention with a stable, predictable API, and Funes expects nothing beyond it — declare attributes and validations exactly as the framework already documents:
# app/models/outstanding_balance.rb
class OutstandingBalance
include ActiveModel::Model
include ActiveModel::Attributes
attribute :outstanding_balance, :decimal
attribute :last_payment_at, :datetime
validates :outstanding_balance, numericality: { greater_than_or_equal_to: 0 }
end
No table, no primary key, no migration. To replay events, the virtual projection only needs to read and write attributes (attributes, assign_attributes) and check whether the resulting state is valid (valid?, errors). ActiveModel::Model and ActiveModel::Attributes provide every one of these.
The projection’s derivative state will be an instance of this model — again, without any side effect.
Wire the materialization model to the projection
Tell the projection which model to materialize with materialization_model:
# app/projections/virtual_outstanding_balance_projection.rb
class VirtualOutstandingBalanceProjection < Funes::Projection
materialization_model OutstandingBalance
interpretation_for Debt::Issued do |state, event, _at|
state.outstanding_balance = event.amount
state
end
interpretation_for Debt::PaymentReceived do |state, event, at|
state.outstanding_balance -= event.principal_amount
state.last_payment_at = at
state
end
end
Because Funes persists nothing, every replay walks the events from scratch and returns a fresh OutstandingBalance instance.
When to use it
- Consistency checks — pair a virtual projection with
consistency_projectionon an event stream, so an invalid resulting state rejects the event before Funes persists it. The validation rules on the model carry the invariants. - One-off queries — when the access pattern doesn’t justify a read table, a virtual projection is faster to set up and has zero schema cost.
- Prototypes — try an interpretation shape before you commit to a migration.
The lack of side effects is exactly what makes a virtual projection perfect as the stream’s consistency_projection. Funes projects the resulting state through it and then calls .valid? on that state — that single check is the gate that decides whether the new event’s append goes through.
When the access pattern does justify a stored representation, switch to a Persistent materialization model — backed by the default database table or a custom destination.