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 DataStructure::Schema

  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. An apply 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 the call instance method) for applying a single event to an entity
  • The class actuator .() (or the class call 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

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 applyand 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

NameDescriptionType
entityInstance of an object or data structure whose state will be mutated by the projection's apply methodsObject

Via the Initializer

self.initialize(entity)

Returns

Instance of the class that includes the EntityProjection module.

Parameters

NameDescriptionType
entityInstance of an object or data structure whose state will be mutated by the projection's apply methodsObject

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:

TagDescription
projectionApplied to all log messages recorded by an entity projection
applyApplied 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:

TagDescription
messageApplied to log messages that record the projection of a typed message instance
message_dataApplied to log messages that record the projection of a MessageData instance
dataApplied 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.