Skip to main content
Version: Master

Custom Repository

Installation with Bundler​

By default RailsEventStore will use the ActiveRecord event repository. If you want to use another event repository without loading unnecessary ActiveRecord dependency, you'll need to do:

gem "rails_event_store", require: "rails_event_store/all"
gem "your_custom_repository"

After running bundle install, Rails Event Store should be ready to be used. See custom repository README to learn how to setup its data store.

Configure custom repository​

Rails.application.configure do
config.to_prepare do
Rails.configuration.event_store = RailsEventStore::Client.new(repository: YourCustomRepository.new)
end
end

Storing events on another database​

By default the ActiveRecord repository reads and writes through models connected the same way as ActiveRecord::Base. To keep the event store on a separate database, define an abstract class with its own connection and hand it to the repository through RubyEventStore::ActiveRecord::WithAbstractBaseClass, which builds the event models on top of it.

class EventStoreRecord < ActiveRecord::Base
self.abstract_class = true
connects_to database: { writing: :events }
end

Rails.configuration.event_store = RailsEventStore::Client.new(
repository: RubyEventStore::ActiveRecord::EventRepository.new(
model_factory: RubyEventStore::ActiveRecord::WithAbstractBaseClass.new(EventStoreRecord),
serializer: RubyEventStore::Serializers::YAML
)
)

The class has to be abstract and inherit from ActiveRecord::Base, otherwise the factory raises ArgumentError.

This covers writing and reading, and nothing else in the Rails Event Store setup has to follow. In particular RailsEventStore::AfterCommitDispatcher needs no matching setting for this — it joins the transaction wrapping your unit of work, which the repository's connection knows nothing about. It may need telling when your own abstract classes use connects_to, which is a separate concern.

A second database costs you atomicity, and it is worth knowing before you reach for one. The repository opens and commits its own transaction on the event store connection, so events are durable the moment publish returns. A business transaction rolling back afterwards takes your records with it and leaves the events behind:

ApplicationRecord.transaction do
order.save!
event_store.publish(OrderPlaced.new(data: { order_id: order.id }))
raise ActiveRecord::Rollback
end

# the order is gone, OrderPlaced is not

On one database the two share a transaction and roll back together. On two there is nothing to make them agree, so treat the events as published the moment they are written.

Writing your own repository​

If you want to write your own repository, we provide a suite of tests that you can re-use. Just require and include it in your repository spec. Make sure to meditate on which expected_version option you are going to support and how.

Using RubyEventStore::InMemoryRepository for faster tests​

RubyEventStore comes with RubyEventStore::InMemoryRepository that you can use in tests instead of the default one. InMemoryRepository does not persist events but offers the same characteristics as RubyEventStore::ActiveRecord::EventRepository. It is tested with the same test suite and raises identical exceptions.

RSpec.configure do |c|
c.around(:each) do |example|
Rails.configuration.event_store = RailsEventStore::Client.new(
repository: RubyEventStore::InMemoryRepository.new
)
# add subscribers here
example.run
end
end

InMemoryRepository defaults to the RubyEventStore::NULL serializer, so event data and metadata are kept as-is instead of being dumped and loaded on every write and read. That is what makes it fast — no extra configuration is needed for it.

We don't recommend using InMemoryRepository in production even if you don't need to persist events because the repository keeps all published events in memory. This is acceptable in testing because you can throw the instance away for every test and garbage collector reclaims the memory. In production, your memory would keep growing until you restart the application server.

Using Ruby Object Mapper (ROM) for SQL without ActiveRecord or Rails​

RubyEventStore comes with RubyEventStore::ROM::EventRepository that you can use with a SQL database without requiring ActiveRecord or when not using Rails altogether. It is tested with the same test suite as the ActiveRecord implementation and raises identical exceptions.

See Using Ruby Event Store without Rails for information on how to use ROM (and Sequel).

Using PgLinearizedEventRepository for linearized writes​

ruby_event_store-active_record comes with additional version of repository named RubyEventStore::ActiveRecord::PgLinearizedEventRepository. It is almost the same as regular active record repository, but has linearized writes to the database and is only restricted to work in PostgreSQL database (as the name suggests).

There are usecases, where you may want to use event store as a queue. For example, you may want to build some read models on separate server and in order to build them correctly, you need to process the facts in the order they were written. In general case it is not that easy, because SQL databases auto-increment rows in the moment of insertion, not commit. So that allows event numbered 42 be already committed, but event numbered 40 still be somewhere in transaction, not readable from outside world. Therefore, the easiest implementation of such queue: "Remember the last processed event id" would not work in that case.

There are many subtleties in this topic, but one of the simplest solutions is to linearize all writes to event store. That's what RubyEventStore::ActiveRecord::PgLinearizedEventRepository is for. Of course by linearizing your writes you lose performance and you make it impossible to scale your application above certain level. As usually, your mileage may vary, but such solution is undoubtedly the simplest and good enough in some usecases.