# Entity Projection
An entity receives its data from events. Each event may have some piece of data to contribute to the entity's state. Multiple events are typically involved in providing an entity with all of its state.
The entity projection is the mechanism that affects an entity with an event's data. The projection applies the event to the entity.
The entity projection applies one event to one entity. To apply multiple events to an entity, the projection is invoked once for each event in sequence.
When an entity is "retrieved" from the message store, it's the events that are retrieved, and then fed one-by-one in the order that they were written and applied to the entity via a projection.
Note: Events should be applied in the same order that they were recorded. This preserves any inherent order of operations of events, and allows the projection to reconstruct the state of the entity correctly.
# Example
class Projection
include EntityProjection
entity_name :account
apply Deposited do |deposited|
account.id = deposited.account_id
amount = deposited.amount
account.deposit(amount)
end
apply Withdrawn do |withdrawn|
account.id = withdrawn.account_id
amount = withdrawn.amount
account.withdraw(amount)
end
end
class Account
include Schema::DataStructure
attribute :id, String
attribute :balance, Numeric, default: -> { 0 }
def deposit(amount)
self.balance += amount
end
def withdraw(amount)
self.balance -= amount
end
end
account = Account.new
deposited = Deposited.new()
deposited.account_id = '123'
deposited.amount = 11
withdrawn = Withdrawn.new()
withdrawn.account_id = '123'
withdrawn.amount = 1
[deposited, withdrawn].each do |event|
Projection.(account, event)
end
account.id
# => "123"
account.balance
# => 10
# Entity Projection Facts
- Projections mutate an entity's state
- An actuation of a projection applies one event to one entity
- An event is ignored if the projection doesn't have a matching
apply
block for the event - Events should be applied in the same order that the events were recorded
- Projections do not interact with the message store
- An
apply
block should not have any logic that decides whether an event should be applied or not. Anapply
block should apply the events that it handles, or not handle them at all.
# EntityProjection Module
A class becomes a projection by including the EntityProjection
module from the EntityProjection
library and namespace.
The EntityProjection
module affords the receiver with:
- The
apply
class macro used for defining event application blocks - The principle instance actuator
.()
(or thecall
instance method) for applying a single event to an entity - The class actuator
.()
(or the classcall
method) that provides a convenient invocation shortcut that does not require instantiating the projection class first - Infrastructure for registering events that are projected, and the dispatching logic used to apply events and event data
# Defining a Projection
# Using the apply Macro
A projection block is defined with the apply
macro.
apply Withdrawn do |withdrawn|
account.id = withdrawn.account_id
amount = withdrawn.amount
account.withdraw(amount)
end
The argument is a class name of the event that the apply block will process. The block argument is the instance of the event being projected.
The macro is merely a code generator that generates an instance method. The example above generates an instance method named apply_withdrawn
. The macro is simply an affordance intended to emphasize the code in a projection class that is directly responsible for event projection. The apply block code is used as the implementation of the generated method.
# Using a Plain Old Method
The apply
macro generates methods with names of the form apply_{event_class_name_underscore_case}
.
Projections can be created by directly defining a method following the naming convention.
def apply_withdrawn(withdrawn)
# ...
end
# Accessing the Entity
# Via the entity Accessor
By default, the entity that the projection is constructed with can be accessed using the entity
method.
entity()
Returns
Instance of the entity that the projection is constructed with.
apply Withdrawn do |withdrawn|
amount = withdrawn.amount
entity.withdraw(amount)
end
# Via the Named Accessor
An alias for the entity
accessor is generated by the entity_name
class macro.
class SomeProjection
include EntityProjection
entity_name :something
apply Deposited do |deposited|
amount = deposited.amount
something.deposit(amount)
end
# Sending an Event to a Projection
There are two ways to send an event to a projection:
- Via an entity store
- Via direct actuation
# Via an Entity Store
An entity store brings event projection and event storage retrieval together in an object that has the feel of a typical data access object.
class Store
include EntityStore
category :account
entity Account
projection Projection
reader MessageStore::Postgres::Read
end
store = AccountStore.build
account = store.fetch('123')
account.id
# => "123"
account.balance
# => 10
Note: See the Entity Store User Guide for more information about entity stores.
# Direct Actuation
A projection can be actuated directly as an object, passing an event as a parameter. Direct actuation is critical for testing and exercising handlers, as it allows handlers to be exercised as plain old objects. It's also a useful technique in some event data analysis scenarios.
Projections can be actuated either via its class interface, as a matter of convenience, or via its instance interface, which allows for greater control of the configuration of the projection.
Projections are implemented as callable objects. Actuating them is simply a matter of invoking their call
method.
some_event = SomeEvent.new
# Via the class interface
SomeProjection.(some_event)
# Via the object interface
some_projection = SomeProjection.build
some_projection.(some_message)
# When a Projection Doesn't Handle a Message
When there isn't a matching apply
block for a message, the projection simply ignores the event sent to it.
# Matching Events to Apply Blocks
When an event is sent to a projection, the projection determines whether there is an apply method that can receive the message.
An apply method is determined to match an inbound event based on the event's class name and the method's name.
An event class named SomeEvent
is sent to an apply method named apply_some_event
.
Only the event's class name is taken into considering when matching a message to a handler method. The class's namespace is not significant to matching. For a message class named Something::Events::SomeEvent
, only the SomeEvent
part of the event's class name is significant.
# Projecting Raw Message Data
In addition to handling typed events, projections can apply MessageData
instances in their raw form.
See the Message and MessageData user guide for more on messages and message data.
The raw form of a message is an instance of MessageStore::MessageData
.
The object that is sent to a projection from an entity store is an instance of MessageData
. The projection converts the MessageData
into its corresponding event message instance.
If a projection implements a method named apply
and if there's no explicit handler block that specifically matches the MessageData
object's type
attribute, then the MessageData
instance will be passed to the apply
method.
def apply(message_data)
case message_data.type
when 'Withdrawn'
# Project Withdrawn
when 'Deposited'
# Project Deposited
end
end
The apply
method will not be invoked if there's an apply block that matches the MessageData
's type
attribute.
class Handler
include Messaging::Handle
apply Withdrawn do |withdrawn|
# ...
end
def apply(message_data)
case message_data.type
when 'Withdrawn'
# This will never be invoked because the apply block
# for Withdraw takes precedence
when 'Deposited'
# This will be called when the type attribute is 'Deposited'
# because there's no apply block for Deposited
end
end
end
# When to Apply Raw Message Data
Because the raw MessageData
is not transformed into typed event messages, projecting MessageData
in its raw form offers a slight performance improvement due to skipping the transformation step.
That said, the performance improvement is negligible. Don't elect to use this option unless squeezing every last drop of performance out of your solution is critical to its success.
# Apply Blocks Return the Input Message
Apply blocks and the apply
method return the message that is the input to the handler.
When the input is an instance of Messaging::MessageData
, and there's a typed apply block that applies the MessageData
's type, the instance of typed message that the MessageData
is converted to will be returned.
When projecting the raw MessageData
using the apply
method, the MessageData
instance is returned.
# Constructing Projections
Projections can be constructed in one of two ways:
- Via the constructor
- Via the initializer
# Via the Constructor
self.build(entity)
The constructor not only instantiates the projection, but also invokes the projection's configure
instance method, which constructs the projection's operational dependencies.
some_projection = SomeProjection.build
Returns
Instance of the class that includes the EntityProjection
module.
Parameters
Name | Description | Type |
---|---|---|
entity | Instance of an object or data structure whose state will be mutated by the projection's apply methods | Object |
# Via the Initializer
self.initialize(entity)
Returns
Instance of the class that includes the EntityProjection
module.
Parameters
Name | Description | Type |
---|---|---|
entity | Instance of an object or data structure whose state will be mutated by the projection's apply methods | Object |
By constructing a handler using the initializer, the handler's dependencies are not set to operational dependencies. They remain inert substitutes.
TIP
See the useful objects user guide for background on inert substitutes.
# Configuring Dependencies
configure()
If the projection implements an instance method named configure
, the build
constructor will invoke it.
The configure
method provides a specialization mechanism for setting up any dependencies, or doing any setup necessary.
dependency :clock, Clock::UTC
def configure
Clock::UTC.configure(self)
end
# Always Project the Entity ID
It's good practice to always project the entity ID in every apply block. It's possible, due to the vagaries of computers, networks, and electricity that events may not be written, and thus projected, in the order that they are presumed to be written. Certain message transport architectures in more elaborate systems topologies may cause this. This is especially true where intermediaries are involved.
The general rule is that the first event in an event stream should establish an entity's ID. Due to the possibilities that events may be in an order other than the one assumed, any event may be the first event projected.
class Projection
include EntityProjection
entity_name :account
apply Deposited do |deposited|
account.id = deposited.account_id
amount = deposited.amount
account.deposit(amount)
end
apply Withdrawn do |withdrawn|
account.id = withdrawn.account_id
amount = withdrawn.amount
account.withdraw(amount)
end
end
If this is not done, and under the right circumstances, an entity may be retrieved in a handler that does not yet have an id. In such a case, the value of the ID used in the handler logic will be nil
. A nil
entity will cause a number of malfunctions that would be difficult - and in some cases, impossible - to correct.
Note: While the circumstances that cause this anomaly are very rare, that rarity should not be counted upon. Projections should be constructed conservatively so that the realities of out-of-order events are never a problem.
# Log Tags
The following tags are applied to log messages recorded by an entity projection:
Tag | Description |
---|---|
projection | Applied to all log messages recorded by an entity projection |
apply | Applied to log messages recorded when applying a typed message or MessageData to an entity |
The following tags may be applied to log messages recorded by an entity projection:
Tag | Description |
---|---|
message | Applied to log messages that record the projection of a typed message instance |
message_data | Applied to log messages that record the projection of a MessageData instance |
data | Applied to log messages that record the data content of a typed message or a MessageData instance |
See the logging user guide for more on log tags.