Skip to content

Latest commit

 

History

History
195 lines (146 loc) · 6.53 KB

README.md

File metadata and controls

195 lines (146 loc) · 6.53 KB

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