This project illustrates how to build an event-driven architecture in Ruby on Rails, following many Doman Driven Design best practices including:
- layered architecture: HTTP, GraphQL, ruby console -> calls stateless application service -> orchestrates models
- modularised monolith: models exist in subdomains, and subdomains communicate with each other indirectly via model events
- aggregate consistency: lock version on all model aggregates protects against multithreading bugs
-
Simplicity: A shared, event-centric conceptual model provides a unified, non-technical understanding across different system components and stakeholders, reducing complexity and facilitating clearer communication.
-
Visibility: By centralizing event logging it becomes easier to monitor, troubleshoot, and debug system issues.
-
Scalability: EDA's are highly scalable. They can handle high volumes of events and adapt to changes in these volumes over time.
-
Decoupling: EDA promotes loose coupling between system components, as each component only knows about the event and how to handle it. This allows developers to work on individual components without impacting the whole system.
-
Resiliency: By separating the event producers from the consumers, the system becomes more resilient. If one part of the system fails, it won't directly affect the others.
Clone the repository and install the dependencies:
git clone https://github.com/fast-programmer/message_driven_app.git
cd message_driven_app
bundle install
After installing, you can create the database and run the migration:
RAILS_ENV=development bin/rake db:create
RAILS_ENV=development bin/rake db:migrate
RAILS_ENV=development bin/rails c
User.create(email: '[email protected]')
# OR
ActiveRecord::Base.transaction do
user = Models::User.create(email: '[email protected]')
user.events.create!(name: 'User.create')
end
SELECT * FROM messages ORDER BY created_at ASC;
irb(main):005:0> Models::User.find(2).events.map { |event| event.name }
=> ["User.created"]
bin/message_publisher
This script enumerates through all unpublished events, calling message handlers in each subdomain
- if the handlers do not throw any exceptions, set the message status to
published
- if the handlers do throw an exception, set the message status to
failed
The publisher broadcasts each event to all subdomain message handlers.
Inside each subdomain message handler, messages are routed to handlers based on event.name
(e.g. User.created
).
This is the best place to call external APIs.
Existing job processing infrastructure such as sidekiq workers can also be integrated.
Main point is that heavy lifting is done outside of web requests, based on events.
Use a pool of worker threads to handle more than 1 event concurrently.
Automate the creation of the schema, model, test, and publisher code through a generator. This generator could be included in a separate gem and required into the main app.
Allow for separate, isolated user spaces.
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.
This project is licensed under the MIT License. See the LICENSE file for details.