# Message Handlers

A message handler (or just "handler") is the entry point of a message into the business logic of a service. It receives instructions from other services, apps, and clients in the form of commands and events.

# Example

class Handler
  include Messaging::Handle
  include Messaging::StreamName

  dependency :write, Messaging::Postgres::Write
  dependency :clock, Clock::UTC
  dependency :store, Store

  def configure
    Messaging::Postgres::Write.configure(self)
    Clock::UTC.configure(self)
    Store.configure(self)
  end

  category :account

  handle Withdraw do |withdraw|
    account_id = withdraw.account_id

    account = store.fetch(account_id)

    time = clock.iso8601

    stream_name = stream_name(account_id)

    unless account.sufficient_funds?(withdraw.amount)
      withdrawal_rejected = WithdrawalRejected.follow(withdraw)
      withdrawal_rejected.time = time

      write.(withdrawal_rejected, stream_name)

      return
    end

    withdrawn = Withdrawn.follow(withdraw)
    withdrawn.processed_time = time

    write.(withdrawn, stream_name)
  end

  handle Deposit do |deposit|
    account_id = deposit.account_id

    time = clock.iso8601

    deposited = Deposited.follow(deposit)
    deposited.processed_time = time

    stream_name = stream_name(account_id)

    write.(deposited, stream_name, expected_version: version)
  end
end

# Handler Facts

  • Handler classes can define more than one handler block
  • There should only be one handler block per message class (duplicate handler blocks will overwrite each other)
  • Handlers may receive message from any number of streams
  • The typical result of processing a message is writing another message, usually an event

# Typical Handler Workflow

  1. Retrieve the entity from the store (which projects the entity's data from its events)
  2. Use the entity to determine whether and how to process the message
  3. Construct the resulting event that captures the effects of processing the message
  4. Assign data to the resulting event from the input message, the system clock, and possibly other sources depending on the business scenario
  5. Write the resulting event

Note that some handlers may not need to do all of these things, and some may do more, like interacting with external APIs and gateways.

# Handler Workflow Sequence Diagram

Handler Workflow Sequence Diagram

# Messaging::Handle Module

A class becomes a handler by including the Handle module from the Messaging library and namespace.

The Messaging::Handle affords the receiver with:

  • The handle class macro used for defining handler blocks
  • The principle instance actuator .() (or the call instance method) for handling a single message
  • The class actuator .() (or the class call method) that provides a convenient invocation shortcut that does not require instantiating the handler class first
  • The handle instance method used for handling raw message data
  • Infrastructure for registering messages that are handled, and the dispatching logic used to handle messages and message data

# Defining a Handler

# Using the handler Macro

A handler block is defined with the handle macro.

handle Withdraw do |withdraw|
  # ...
end

The argument is a class name of the message that the handler block will process. The block argument is the instance of the message being processed.

The macro is merely a code generator that generates an instance method. The example above generates an instance method named handle_withdraw. The macro is simply an affordance intended to emphasize the code in a handler class that is directly responsible for message processing. The handler block code is used as the implementation of the generated method.

# Using a Plain Old Method

The handle macro generates methods with names of the form handle_{message_class_name_underscore_case}.

Handlers can be created by directly defining a method following the naming convention.

def handle_withdraw(withdraw)
  # ...
end

# Sending a Message to a Handler

There are two ways to send a message to a handler:

# Via a Consumer

Registering handlers with consumers is how handlers process messages in live services. The combination of the consumer and the component host infrastructure will route messages from the streams that the consumer reads to the consumer's handlers.

class Consumer
  include Consumer::Postgres

  handler SomeHandler
  handler SomeOtherHandler
end

# Direct Actuation

A handler can be actuated directly as an object, passing a message as a parameter. Direct actuation is critical for testing and exercising handlers, as it allows handlers to be exercised as plain old objects.

A handler 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 handler.

Handlers are implemented as callable objects. Actuating them is simply a matter of invoking their call method.

some_message = SomeMessage.new

# Via the class interface
SomeHandler.(some_message)

# Via the object interface
some_handler = SomeHandler.build
some_handler.(some_message)

# When a Handler Doesn't Handle a Message

When there isn't a matching handler method for a message, the handler simply ignores the message sent to it.

# Optional Strict Handling

The handler's actuator provides an optional keyword argument named strict.

Handling a message when strict is set to true will require that the handler class implements a handler block for the message class.

With strict set to true, and a message sent to a handler that the handler doesn't handle, an error will be raised.

SomeHandler.(some_unhandled_message, strict: true)
# => SomeHandler does not implement a handler for SomeUnhandledMessage. Cannot handle the message. (Messaging::Handle::Error)

This argument is available for both the class actuator and the instance actuator.

# HANDLE_STRICT Environment Variable

The HANDLE_STRICT environment variable can be used to set handlers' strictness.

It can be set to one of the following values:

  • on
  • off

The value defaults to off.

HANDLE_STRICT=on start_service.sh

# Matching Messages to Handlers

When a message is sent to a handler, the handler determines whether there is a handler method that can receive the message.

A handler method is determined to match an inbound message based on the message's class name and the method's name.

A message class named SomeMessage is sent to a handler method named handle_some_message.

Only the message'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::Messages::SomeMessage, only the SomeMessage part of the message's class name is significant.

# Handling Raw Message Data

In addition to handling typed messages, handlers can handle 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 handler from a consumer is an instance of MessageData. The handler converts the MessageData into its corresponding message instance.

If a handler implements a method named handle 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 handle method.

def handle(message_data)
  case message_data.type
  when 'Withdraw'
    # Handle Withdraw
  when 'Deposit'
    # Handle Deposit
  end
end

The handle method will not be invoked if there's a handler block that matches the MessageData's type attribute.

class Handler
  include Messaging::Handle

  handle Withdraw do |withdraw|
    # ...
  end

  def handle(message_data)
    case message_data.type
    when 'Withdraw'
      # This will never be invoked because the handler block
      # for Withdraw takes precedence
    when 'Deposit'
      # This will be called when the type attribute is 'Deposit'
      # because there's no handler block for Deposit
    end
  end
end

# When to Handle Raw Message Data

Because the raw MessageData is not transformed into typed messages, handling 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.

# Handler Blocks Return the Input Message

Handler blocks and the handle 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 handler block that handles the MessageData's type, the instance of typed message that the MessageData is converted to will be returned.

When handling the raw MessageData using the handle method, the MessageData instance is returned.

# Exiting a Handler Block Using Return

A handler can be exited simply by using a return statement.

This is true because the handle macro generates a plain old method. Issuing a return from within the block is effectively the same as returning from a method.

Note: Handler blocks and handler methods are not expected to return any values. Any value that is returned from a handler block or method is disregarded.

# Constructing Handlers

Handlers can be constructed in one of two ways:

  • Via the constructor
  • Via the initializer

# Via the Constructor

self.build(strict: false, session: nil)

The constructor not only instantiates the handler, but also invokes the handler's configure instance method, which constructs the handler's operational dependencies.

some_handler = SomeHandler.build

Returns

Instance of the class that includes the Handle module.

Parameters

Name Description Type
strict Strict mode, causes an error when no handler block for the message is implemented Boolean
session An existing session object to use, rather than allowing the handler's dependencies to create a new session Session

# Via the Initializer

self.initialize()

Returns

Instance of the class that includes the Handle module.

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(session: nil)

Parameters

Name Description Type
session If a session is provided to the handler's constructor, it will be passed to the instance's configure method MessageStore::Postgres::Session

If the handler implements an instance method named configure, the build constructor will invoke it.

The configure method provides a specialization mechanism for setting up any handler dependencies, or doing any setup necessary.

dependency :write, Messaging::Postgres::Write
dependency :clock, Clock::UTC
dependency :store, Store

def configure(session: nil)
  Messaging::Postgres::Write.configure(self)
  Clock::UTC.configure(self)
  Store.configure(self, session: session)
end

# Messaging::StreamName Module

The StreamName module from the Messaging library and namespace provides a couple useful utilities to handler classes. Using this module in a handler is optional.

The feature of the StreamName module that is most commonly used in handlers is the stream_name method.

The stream_name method is used to combine a category name and an entity ID to form a valid and consistent stream name to write a handler block's resulting events to.

The stream_name method uses the category name declared using the category macro to compose the stream name.

account_id = '123'
stream_name = stream_name(account_id)
# => "account-123"

Optionally, a category other than the one declared using the category macro can be passed as a second argument.

some_id = '456'
stream_name = stream_name(some_id, :something)
# => "something-123"

See the Messaging::StreamName topic for more.

# Messaging::Category Module

The Category module from the Messaging library and namespace provides the category class macro to classes that include the module.

The Category module is included into any class that includes the Messaging::StreamName module

The category macro allows the declaration of the category that a handler (or other receiver of the mixin) is principally concerned with.

In the following code taken from the handler example above, :account is declared as the category.

category :account

The category macro creates an instance accessor that returns the value passed to the macro. This value is used when composing the stream name that messages are typically written to by the handler logic.

See the Messaging::Category topic for more.

# Log Tags

The following tags are applied to log messages recorded by a handler:

Tag Description
handle Applied to all log messages recorded by a handler
dispatch Applied to log messages recorded while determining if a message will be handled
messaging Applied to all log messages recorded inside the Messaging namespace

The following tags may be applied to log messages recorded by a handler:

Tag Description
message_data Applied to log messages that address the handling of a MessageData instance
message Applied to log messages that address the handling of a typed message
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.