{"id":15584097,"url":"https://github.com/stevo/pubsub_on_rails","last_synced_at":"2025-09-03T20:50:23.212Z","repository":{"id":44535762,"uuid":"185214708","full_name":"stevo/pubsub_on_rails","owner":"stevo","description":"Gem facilitating opinionated approach to leveraging publish/subscribe messaging pattern in Ruby on Rails applications.","archived":false,"fork":false,"pushed_at":"2023-12-11T10:52:36.000Z","size":47,"stargazers_count":45,"open_issues_count":2,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-24T04:13:07.038Z","etag":null,"topics":["patterns","pubsub","ror","ruby","ruby-on-rails"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stevo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-05-06T14:36:44.000Z","updated_at":"2024-08-16T15:53:56.000Z","dependencies_parsed_at":"2023-12-11T11:47:49.821Z","dependency_job_id":"d28fbfd6-31d5-4d13-b8cb-c5885c9b61db","html_url":"https://github.com/stevo/pubsub_on_rails","commit_stats":{"total_commits":22,"total_committers":3,"mean_commits":7.333333333333333,"dds":0.5,"last_synced_commit":"6fc8b9d38decf9a66d9a75dd8111b1b4fcf10624"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/stevo/pubsub_on_rails","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevo%2Fpubsub_on_rails","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevo%2Fpubsub_on_rails/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevo%2Fpubsub_on_rails/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevo%2Fpubsub_on_rails/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stevo","download_url":"https://codeload.github.com/stevo/pubsub_on_rails/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevo%2Fpubsub_on_rails/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273509123,"owners_count":25118447,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-03T02:00:09.631Z","response_time":76,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["patterns","pubsub","ror","ruby","ruby-on-rails"],"created_at":"2024-10-02T20:23:01.777Z","updated_at":"2025-09-03T20:50:23.186Z","avatar_url":"https://github.com/stevo.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# PubSub on Rails\n\nPubSub on Rails is a gem facilitating opinionated approach to leveraging publish/subscribe messaging pattern in Ruby on Rails applications.\n\nThere are many programming techniques that are powerful yet complex. The beauty of publish/subscribe patterns is that it is powerful while staying simple.\n\nInstead of using callbacks or directly and explicitly executing series of actions, action execution is requested using an event object combined with event subscription.\nThis helps in keeping code isolation high, and therefore makes large codebases maintainable and testable.\n\nWhile it has little to do with event sourcing, it encompasses a couple of ideas related to domain-driven development.\nTherefore it is only useful in applications in which domains/bounded-contexts can be identified.\nThis is especially true for applications covering many side effects, integrations and complex business logic.\n\n## Installation\n\n```ruby\n# Gemfile\n\ngem 'pubsub_on_rails', '~\u003e 1.1.0'\n\n# config/initializers/pub_sub.rb\n\nrequire 'pub_sub/subscriptions_list'\n\nRails.configuration.to_prepare do\n  Rails.configuration.event_store = event_store = RailsEventStore::Client.new(\n    repository: RailsEventStoreActiveRecord::EventRepository.new(serializer: RubyEventStore::NULL)\n  )\n\n  PubSub::SubscriptionsList.config_path =\n    Rails.root.join('config/subscriptions.yml')\n  PubSub::SubscriptionsList.load!(event_store)\nend\n```\n\n## Migrating from 0.0.7 to 1.0.0\n\n1. Update gem to version `1.0.0`\n\n```ruby\n# Gemfile\n\ngem 'pubsub_on_rails', '~\u003e 1.0.0'\n```\n\n2. Run Rails Event Store migrations\n\n**MySQL**\n```\nbin/rails generate rails_event_store_active_record:migration\nbin/rails db:migrate\n```\n\n**PostgreSQL**\n```\nbin/rails generate rails_event_store_active_record:migration --data-type=jsonb\nbin/rails db:migrate\n```\n\n3. Update initializer to use Rails Event Store Client\n\n```ruby\n# config/initializers/pub_sub.rb\n\nrequire 'pub_sub/subscriptions_list'\n\nRails.configuration.to_prepare do\n  Rails.configuration.event_store = event_store = RailsEventStore::Client.new(\n    repository: RailsEventStoreActiveRecord::EventRepository.new(serializer: RubyEventStore::NULL)\n  )\n\n  PubSub::SubscriptionsList.config_path =\n    Rails.root.join('config/subscriptions.yml')\n  PubSub::SubscriptionsList.load!(event_store)\nend\n```\n\n4. Override `EventWorker` or override `EventHandlerBuilder` if needed\n\nFor example when you want to have different workers for different events:\n\n```ruby\n# config/initializers/pub_sub.rb\n\nPubSub::EventHandlerBuilder.class_eval do\n  def call(event)\n    if async?\n      if class_name.to_s.include?('MyType')\n        SingleThreadEventWorker.perform_in(2.seconds, class_name.to_s, event.event_id)\n      else\n        EventWorker.perform_in(2.seconds, class_name.to_s, event.event_id)\n      end\n    else\n      class_name.new(event).call!\n    end\n  end\nend\n```\n\n5. Add event objects for Rails Event Store streams. Check [Event](README.md#Event) section.\n6. Update test cases to use new matchers. Check [Testing](README.md#Testing) section.\n\n## Entities\n\nThere are five entities that are core to PubSub on Rails: domains, events, event publishers, event handlers and subscriptions.\n\n### Domain\n\nDomain is simply a named context in application. You can refer to it as \"module\", \"subsystem\", \"engine\", whatever you like.\nGood names for domains are \"ordering\", \"messaging\", \"logging\", \"accounts\", \"logistics\" etc.\nYour app does not need to have code isolated inside domains, but using Component-Based Rails Applications concept (CBRA) sounds like a nice idea to be combined with PubSub on Rails.\n\nDomain example:\n\n```ruby\n# app/domains/messaging.rb\n\nmodule Messaging\nend\n```\n\n### Event\n\nEvent is basically an object indicating that something has happened (event has occured).\nThere are two important things that need to be considered when planning an event: its **name** and its **payload** (fields).\n\nName of event should describe an action that has just happened, also it should be namespaced with the name of the domain it has occurred within.\nSome examples of good event names: `Ordering::OrderCancelled`, `Messaging::IncorrectLoginNotificationSent`, `Accounts::UserCreated`, `Bookings::CheckinDateChanged`, `Reporting::MonthlySalesReportGenerationRequested`\n\nPayload of event is just simple set of fields that should convey critical information related to the event.\nAs the payload is very important for each event (it acts as a contract between publisher and handler), PubSub on Rails leverages `Dry::Struct` and `Dry::Types` to ensure both presence and correct type of attributes events are created with.\nIt is a good rule of a thumb not to create too many fields for each event and just start with the minimal set. It is easy to add more fields to event's payload later (while it might be cumbersome to remove or change them).\n\nEvent example:\n\n```ruby\n# app/events/ordering/order_created_event.rb\n\nmodule PubSub\n  module Ordering\n    class OrderCreatedEvent \u003c PubSub::EventWithType\n      schema do\n        attribute :order_id, Types::Strict::Integer\n        attribute :customer_id, Types::Strict::Integer\n        attribute :line_items, Types::Strict::Array\n        attribute :total_amount, Types::Strict::Float\n        attribute :comment, Types::Strict::String.optional\n      end\n    end\n  end\nend\n```\n\nSince we are using Rails Event Store to handle events, it gives us a possibility to create **stream** of events. We can treat them as sub-list of events. To be able to use that functionality we need to declare which streams given event should be part of. By default we add event to stream based on its name. In case of our example it is `ordering__order_created`. We can provide also custom streams even based on some additional data from the event attributes (for example to group all events related to given order).\n\nEvent example:\n\n```ruby\n# app/events/rails_event_store/ordering/order_created_event.rb\n\nmodule PubSub\n  module Ordering\n    class OrderCreatedEvent \u003c PubSub::EventWithType\n      def stream_names\n        [\n          \"order__#{data[:order_id]}\"\n        ]\n      end\n    end\n  end\nend\n```\n\n### Event publisher\n\nEvent publisher is any class capable of emitting an event.\nUsually a great places to start emitting events are model callbacks, service objects or event handlers.\nIt is very preferable to emit one specific event from only one place, as in most cases this makes the most sense and makes the whole solution more comprehensible.\n\nEvent publisher example:\n\n```ruby\n# app/models/order.rb\n\nclass Order \u003c ApplicationRecord\n  include PubSub::Emit\n\n  belongs_to :customer\n  has_many :line_items\n\n  #...\n\n  after_create do\n    emit(:ordering__order_created, order_id: id)\n  end\nend\n```\n\n### Event handler\n\nEvent handler is a class that encapsulates logic that should be executed in reaction to event being emitted.\nOne event can be handled by many handlers, but only one unique handler within each domain.\nEvent handlers can be executed synchronously or asynchronously. The latter is recommended for both performance and error-recovery reasons.\n\nEvent handler example:\n\n```ruby\n# app/event_handlers/messaging/ordering_order_created_handler.rb\n\nmodule Messaging\n  class OrderingOrderCreatedHandler \u003c PubSub::DomainEventHandler\n    def call\n      OrderMailer.order_creation_notification(order).deliver_now\n    end\n\n    private\n\n    def order\n      Order.find(event_data.order_id)\n    end\n  end\nend\n```\n\nAll fields of event's payload are accessible through `event_data` method, which is a simple struct.\n\n#### Conditionally processing events\n\nIf in any case you would like to control if given handler should be executed or not (maybe using feature flags), you can override `#process_event?` method.\n\n```ruby\n# app/event_handlers/messaging/ordering_order_created_handler.rb\n\nmodule Messaging\n  class OrderingOrderCreatedHandler \u003c PubSub::DomainEventHandler\n    # ...\n\n    private\n\n    def process_event?\n      Features.notifications_enabled?\n    end\n  end\nend\n```\n\n### Subscription\n\nSubscription is \"the glue\", the binds events with their corresponding handlers.\nEach subscription binds one or all events with one handler.\nSubscription defines if given handler should be executed in synchronous or asynchronous way.\n\nSubscription example:\n\n```yaml\n# config/subscriptions.yml\n\nmessaging:\n  ordering__order_created: async\n```\n\n## Testing\n\nMost of entities in Pub/Sub approach should be tested, yet both domain and event classes can be tested implicitly.\nIt is recommended to start testing from testing subscription itself, then ensure that both event emission and handling are in place. Depending on situation the recommended order may change though.\n\n### RSpec\n\nThe recommended RSpec configuration is as follows:\n\n```ruby\n# spec/support/pub_sub.rb\n\nrequire 'pub_sub/testing'\n\nRSpec.configure do |config|\n  config.include PubSub::Testing::RailsEventStore\n  config.include PubSub::Testing::EventDataHelper\n\n  config.around(:each, in_memory_res_client: true) do |example|\n    current_event_store = Rails.configuration.event_store\n    Rails.configuration.event_store = RubyEventStore::Client.new(\n      repository: RubyEventStore::InMemoryRepository.new\n    )\n    example.run\n    Rails.configuration.event_store = current_event_store\n  end\nend\n```\n\nThis will allow you to use `in_memory_res_client` which will not create object (event) in the database and do not call all dependent logic (handlers).\n\n### Testing subscription\n\nTesting subscription is as easy as telling what domains should subscribe to what event in what way.\n\nExample:\n\n```ruby\nRSpec.describe Messaging do\n  it { is_expected.to subscribe_to(:ordering__order_created).asynchronously }\nend\n```\n\n### Testing publishers\n\nTo test publisher it is crucial to test if event was emitted under certain conditions (if any).\n\nExample:\n\n```ruby\nRSpec.describe Order do\n  describe 'after_create' do\n    it 'emits ordering__order_created' do\n      customer = create(:customer)\n      line_items = create_list(:line_item, 2)\n\n      Order.create(\n        customer: customer,\n        total_amount: 100.99,\n        comment: 'Small order',\n        line_items: line_items\n      )\n\n      expect(event_store).to have_published(\n        an_event(PubSub::Ordering::OrderCreatedEvent).with_data(\n          order_id: fetch_next_id_for(Order),\n            total_amount: 100.99,\n            comment: 'Small order',\n            line_items: line_items\n        )\n      ).in_stream('ordering__order_created')\n    end\n  end\nend\n```\n\n### Testing handlers\n\nHandlers can be tested by testing their `call!` method, that calls `call` behind the scenes.\nTo ensure event payload contract is met, please use `event_data_for` helper to build event payload hash.\nIt will instantiate event object behind the scenes to ensure it exists and its payload requirements are met.\n\nExample:\n\n```ruby\nmodule Messaging\n  RSpec.describe OrderingOrderCreatedHandler do\n    describe '#call!' do\n      it 'delivers order creation notification' do\n        order = create(:order)\n        event_data = event_data_for(\n          'ordering__order_created',\n          order_id: order.id,\n          total_amount: 100.99,\n          comment: 'Small order',\n          line_items: [build(:line_item)]\n        )\n        order_creation_notification = double(:order_creation_notification, deliver_now: true)\n        allow(OrderMailer).to receive(:order_creation_notification).\n          with(order).and_return(order_creation_notification)\n\n        OrderingOrderCreatedHandler.new(event_data).call!\n\n        expect(order_creation_notification).to have_received(:deliver_now)\n      end\n    end\n  end\nend\n```\n\n### Subscriptions linting\n\nIt is a common problem to implement a publisher and handler and forget about implementing subscription.\nWithout proper integration testing the problem might stay undetected before identified (hopefully) during manual testing.\nThis is where subscriptions linting comes into play. All existing event handlers will be verified against registered subscriptions during linting process.\nIn case of any mismatch, exception will be raised.\n\nTo lint subscriptions, place `PubSub::Subscriptions.lint!` for instance in your `rails_helper.rb` or some initializer of choice.\n\n## Logger\n\nEven though default domain always routes event subscriptions to correspondingly named event handlers, it is possible to implement domains that will route subscriptions in the different way.\nThe simplest way is to define it manually:\n\n```ruby\n# app/domains/logging.rb\n\nmodule Messaging\n  def self.ordering__order_created(event_payload)\n    # whatever you need goes here\n  end\nend\n```\n\nThis technique can be useful for instance for logging\n\n```ruby\n# app/domains/logging.rb\n\nmodule Logging\n  def self.event_logger\n    @event_logger ||= Logger.new(\"#{Rails.root}/log/#{Rails.env}_event_logger.log\")\n  end\n\n  def self.method_missing(method_name, *event_data)\n    event_logger.info(\"Evt: #{method_name}: \\n#{event_data.map(\u0026:to_json).join(', ')}\\n\\n\")\n  end\n\n  def self.respond_to_missing?(method_name, include_private = false)\n    method_name.to_s.start_with?(/[a-z_]+__/) || super\n  end\nend\n```\n\n```yaml\n# config/subscriptions.yml\n\nlogging:\n  all_events: sync\n```\n\n## Payload verification\n\nEvery time event is emitted, its payload is supplied to corresponding `Dry::Struct` event class and is verified.\nThis ensures that whenever we emit event we can be sure its payload is matching specification.\n\nExample:\n\n```ruby\nmodule PubSub\n  module Accounts\n    class PersonCreatedEvent \u003c PubSub::EventWithType\n      schema do\n        attribute :person_id, Types::Strict::Integer\n      end\n    end\n  end\nend\n```\n\n* `emit(:accounts__person_created, person_id: 1)` is ok\n* `emit(:accounts__person_created)` will result in ```PubSub::EventEmission::EventPayloadArgumentMissing: Event [Accounts::PersonCreatedEvent] expects [person_id] payload attribute to be either exposed as [person] method in emitting object or provided as argument```\n* `emit(:accounts__person_created, person_id: 'abc')` will result in ```Dry::Struct::Error: [Accounts::PersonCreatedEvent.new] \"abc\" (String) has invalid type for :person_id violates constraints (type?(Integer, \"abc\") failed)```\n\n## Automatic event name prefixing\n\nWhen you namespace your code to match your domain names, you can skip prefixing an event name with domain name when emitting it.\n\n```ruby\n# app/models/oriering/order.rb\n\nmodule Ordering\n  class Order \u003c ApplicationRecord\n    include PubSub::Emit\n\n    after_create do\n      emit(:order_created, order_id: id)\n      # emit(:ordering__order_created, order_id: id) # this will work as well\n    end\n  end\nend\n```\n\n## Automatic event payload population\n\nWhenever you emit an event, it will try to populate its payload with data using public interface of object it is emitted from within.\n\n```ruby\n# app/models/oriering/order.rb\n\nmodule Ordering\n  class Order \u003c ApplicationRecord\n    include PubSub::Emit\n\n    after_create do\n      emit(:order_created, order_id: id)\n      # emit(\n      #   :ordering__order_created,\n      #   order_id: id, # `self` does not implement `order_id`, therefore value has to be provided explicitly here\n      #   total_amount: total_amount, # attribute matches the name of method on `self`, therefore it can be skipped\n      #   comment: comment # same here\n      # )\n    end\n  end\nend\n```\n\n# TODO\n\n- Dynamic event classes\n- TYPES declaration\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevo%2Fpubsub_on_rails","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstevo%2Fpubsub_on_rails","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevo%2Fpubsub_on_rails/lists"}