Replay
Replay is a gem to support event sourcing data within domain boundaries. With event sourced data, the application stores data as a series of domain events, which are applied to the domain object in order to mutate state.
Disclaimer (5/30/2017)
This repo is here primarily for historical reasons—if you are starting a new project in Ruby and want to eventsource some of your data, I recommend checking out Eventide Project and the various libraries that make up that project, which is (as of this writing) under active development by individuals who are much more concerned than I am with moving Ruby forward in that direction.
CQRS/ES 30 second intro
Command Query Responsibility Segregation (and Fowler's explanation is a pattern popularized by Greg Young and Udi Dahan from within the sphere of Domain Driven Design. The general idea is that within domain models, objects are rarely good at both representing truth and being purposeful for queries and reporting, and therefore we should separate the responsibilities.
Event Sourcing is a pattern that is not required by (but pairs extremely well with) CQRS. However, by embracing this pattern a system can adapt to new reporting and query requirements at any time with a great deal of flexibility, and the use of messaging/pub-sub along with events creates an easy path to breaking apart monolithic applications and separating domains.
A short example
class ReplayExample
include Replay::Publisher
#define events
events do
SomethingHappened(name: String, pid: Integer)
SomethingElseHappened(pid: Integer)
#....
end
#applying events (changing state)
apply SomethingHappened do |event|
@name = event.name
@pid = event.pid
end
apply SomethingElseHappened do |event|
@state = :happened_again
@pid = nil if event.pid == @pid
end
def do_something(pid = nil)
#the command validates inputs
#InvalidCommand is defined by the application
raise InvalidCommand.new("parameters were invalid") unless pid
#publish events
publish SomethingHappened.new(:name = "foo", :pid => pid)
#publish with method syntax
publish SomethingElseHappened(pid: pid)
end
end
There's a couple of things to note about the above example. ReplayExample is a domain object. (Clearly this example is a bit contrived.) Domain objects represent and encapsulate domain logic in its purest sense. No application code should make its way into a domain object, nor should concerns from another bounded context.
Domain objects publish events in order to mutate state. The events published by this domain object are defined within the events
block; ReplayExample::SomethingHappened
is a class defined there, which has two attributes name
and pid
, which are String
and Integer
respectively. Events may also be defined manually, like any other class. Because they're essentially value objects, with zero behavior, the shorthand form above is usually going to be easier.
ReplayExample
instances change state by applying events. These events are handled in the apply
blocks in the above example (you see what I did there?) This part is, mostly, really simple. You probably did a lot of this state thing in your freshman programming class. More on that later.
So if we've got the events defined, and we know what events change state in which ways, where do they come from? Commands, of course. The role of a command is to validate its inputs and publish the events if the command is valid. That's it. No changing state allowed there—seriously, none. Ever heard the term snowflake server? Break the state rule and you're going to have snowflake instances and weird bugs.
Commands are the art and science of CQRS. In the above example, I've implemented it as a method on the domain object (which is also called an aggregate root in the language of DDD.) Its just as frequently done as a class, e.g.
class ReplayExample::DoSomething
include Replay::Publisher
def initialize(name, pid=nil)
raise InvalidCommand.new unless pid
@name = name
@pid = pid
end
def perform
#the publish the event, but don't raise an error if an application block can't be found
publish ReplayExample::SomethingHappened.new(name: @name, pid: @pid), false
end
end
ReplayExample::DoSomething.new("foo", 123).perform
The above command class performs the same function, but has some advantages. In a Rails application, you can mix in ActiveModel::Validations to get ActiveRecord-style validators on it. You can also use Virtus (recommended) or ActiveModel to make it ActiveModel compliant and use it as a form object. This pattern is especially useful when you're dealing with non-domain services (e.g. credit card processors.) You can publish events from any model; there's nothing special about that (though its best if you don't do it without good reason, or you'll subvert one of the great advantages of DDD—separation of bounded contexts).
Digging deeper
The Repository
The Repository is an application-defined object (replay will generate one for you) which will load your domain objects from storage. The repository's job is to find the event stream requested and apply the events from the event stream to a newly created object of the supplied type. Every application has at least one repository, and may have several.
Use it like so:
example = Repository.load(ReplayExample, some_guid)
What you'll get back is a newly initialized instance of your object, with all events from the stream applied in sequence. By default, if it doesn't find any events for that stream identifier, it will raise an exception; you can change this behavior by supplying :create => false
or :create => true
to load
. When false, the Repository will not attempt to create the instance. If true, and the object defines a create
method that takes no parameters, the default implementation will call create
. (Its standard practice for that method to publish a Created
event.)
Your application's repository will look something like this:
class Repository
include Replay::Repository
configure do |config|
config.store = :active_record
config.add_default_subscriber EventLogger
end
end
You can also create a repository for your test environment (though for unit tests its typically unnecessary and for higher levels adding a subscriber will suffice. For example, in Cucumber or its analogues:
#features/env.rb
Repository.configuration.add_default_listener EventMonitor.new
Observers
Replay provides a default message router for observers of events.
In your repository implementation, add :replay_router to the configuration's default subscribers:
class Repository
include Replay::Repository
configure do |config|
config.add_default_subscriber :replay_router
end
end
In your application or domain services:
class MailService
include Replay::Observer
observe Model::EventHappened do |event|
#handle the event
end
end
It may be advantageous in some situations to create multiple routers:
class InternalRouter
include Replay::Router
end
class Repository
include Replay::Repository
configure do |config|
config.add_default_subscriber InternalRouter
end
end
class MailService
include Replay::Observer
router InternalRouter
#observations...
end
Additional gems
replay-rails provides a very basic ActiveRecord-based event store. Its a good template for building your own event store and light duties in an application in which aggregates don't receive hundreds or thousands of events.
TODO
- Implement snapshots for efficient load from repository
- Better documentation
- Build a demonstration app