Skip to content

The Culture Amp Ruby Event Framework ❤️

Notifications You must be signed in to change notification settings

cultureamp/event_framework

Repository files navigation

EventFramework

EventFramework is the event-sourcing framework developed by Culture Amp. The API has been designed to make the happy-path simple, and the complex-path uncomplicated.

Contributing to this framework

The follow commands make it possible for you to run tests and develop this library further:

  • bin/setup
  • bin/task_runner reset_all
  • rspec

Domain Objects

In order to reduce the risk of naming collisions with existing murmur code, domain objects should be implemented in a module, under Domains, named after the primary Aggregate:

module Domains
  module Person
    class EmailAddressChanged < EventFramework::DomainEvent
    end

    class ChangeEmailAddressCommand < EventFramework::Command
    end

    class ChangeEmailAddressCommandHandler < EventFramework::CommandHandler
    end

    class PersonAggregate < EventFramework::Aggregate
    end
  end
end

As you can see from the example above, EventFramework provides several base classes that can be used to build domain objects.

Command

Command is a means of describing and encapsulating the data required when executing a command against an aggregate.

class ChangeEmailAddressCommand < EventFramework::Command
  attribute :person_id, Types::UUID
  attribute :email_address, Types::Strict::String
end

Command is implemented as a dry-struct, allowing us to build concise, self-documenting, mostly-type-safe data objects.

CommandHandler

In general terms, a CommandHandler acts as the bridge between an external source of input and and the intended aggregate. In our current use-case, this means Rails controller actions.

The CommandHandler negotiates the process of instantiating an aggregate and re-building its internal state from the events in the event store:

class ChangeEmailAddressCommandHandler < EventFramework::CommandHandler
  def handle(command)
    metadata.causation_id = "d1bee3f5-0ce6-4483-bd54-8f007260ee19"

    with_aggregate(Person, command.person_id) do |person|
      person.change_email_address(command: command, metadata: metadata)
    end
  end
end

Instances of CommandHandler also provide a metadata object, which can be used to capture request-level data for persistence into the event store.

EventFramework includes a module (TODO: add link once merged) that can be included in a Rails controller and will help instantiate a CommandHandler that is pre-seeded with all the required metadata.

Aggregate

All aggregates inherit from the Aggregate base class. Building internal state is handled by a collection of event handlers, defined on the class using the apply helper method:

class PersonAggregate < EventFramework::Aggregate
  apply :EmailAddressChanged do |event|
    @email_address = event.email_address
  end
end

Commands are implemented as standard methods on the aggregate class. Persisting the event to the event store is handled by calling sink_event.

class PersonAggregate < EventFramework::Aggregate
  def change_email_address(command:, metadata:)
    raise EmailNotChanged if command.email_address == @email_address

    sink_event Events::EmailAddressChanged.new(email_address: command.email_address), metadata
  end
end

If you are implementing a command that will generate multiple events witin that command, each event can be staged by calling stage_event, and then persisted by calling sink_staged_events

class PersonAggregate < EventFramework::Aggregate
  def modify_attributes(command:, metadata:)
    stage_event Events::AlignmentChanged(alignment: command.alignment) if @alignment != command.alignment
    stage_event Events::OriginStoryChanged(origin_story: command.origin_story) if @origin_story != command.origin_story

    sink_staged_events
  end
end

Domain Events

In the example above, you would have noticed a class called Events::EmailAddressChanged being instantiated within the command.

EventFramework refers to these classes as Domain Events.

Every Domain Event that can be generated by our platform is described by a single Ruby class (inheriting from DomainEvent) that belongs to the Events module:

module Events
  class EmailAddressChangedForPerson < EventFramework::DomainEvent
    attribute :new_email_address, Types::Strict::String
  end
end

As with Command, DomainEvent uses dry-struct under the hood.

Internal Persistence

When domain events (and associated metadata) are sunk into the event store, they are persisted into a PostgreSQL database.

When sourced from the event store, they are encapsulated in a generic Event object. Event also contains metadata and additional details from the database:

Attribute Description
id The primary key of the event; UUIDv4, automatically generated by the database
sequence The position of this event in the entire event stream; Automatically generated by the database
aggregate_id The ID of the aggregate that this event pertains to
aggregate_sequence The position of this event within the aggregate-specific event stream. Integer, generated within the aggregate.
domain_event An instance of the DomainEvent class, populated with the contents of the event body.
metadata A Struct that contains the following pieces of metadata:

Event Metadata

Attribute Description
user_id The ID of the User who performed the action. Currently taken from Mumur's authn system.
correlation_id The correlation ID of the event; Usually generated as a unique request ID in the client, and passed via the HTTP request
causation_id The ID of the Event that caused this Event to be created via a Reactor.
created_at The time and date (in UTC) that event saved to the database; Automatically generated by the database.

Running tests

To run the tests, you need a Postgres installed with the [uuid-ossp][https://www.postgresql.org/docs/10/uuid-ossp.html] extension enabled.

Create the test databases:

createdb event_framework_projections_test
createdb event_framework_event_store_test

Migrate the databases:

bundle exec sequel -m db/projections/migrations postgres://localhost/event_framework_projections_test
bundle exec sequel -m db/event_store/migrations postgres://localhost/event_framework_event_store_test

Run the tests:

bundle exec rspec spec

About

The Culture Amp Ruby Event Framework ❤️

Resources

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy