Your Rails app isn’t just a website—it’s a nervous system.
Every time you:
- Call
user.update!(...)
and trigger five side effects... - Chain
after_commit
callbacks until they resemble spaghetti... - Add "just one more thing" to a service object...
...you’re building a tightly coupled time bomb.
Event-Driven Architecture (EDA) flips the script. Instead of:
def checkout_order
order.process_payment! # Direct control
order.send_receipt! # More direct control
order.update_inventory! # Who invited this guy?
end
You broadcast:
event = OrderPaid.new(order_id: order.id)
Rails.configuration.event_store.publish(event)
Now let’s implement this without overengineering it.
Why Rails Needs EDA
The Pain Points
-
Callback Hell:
after_save
chains that break unpredictably. - Testing Nightmares: Need to stub 10 objects to test one feature.
- Scaling Limits: HTTP request timeout? Say goodbye to async workflows.
The Fix
- Decouple components using events ("OrderPaid", "UserBlocked").
- Process async with subscribers (no more 30s request timeouts).
- Replay events for debugging (impossible with HTTP calls).
Tool Showdown
1. Rails Event Store (RES)
Best for: Rails apps dipping toes into EDA.
# Define an event
class OrderPaid < RailsEventStore::Event
def self.strict(data)
new(data.merge(metadata: { timestamp: Time.now }))
end
end
# Publish
event = OrderPaid.strict(order_id: 123)
Rails.configuration.event_store.publish(event)
# Subscribe
module Payments
class OnOrderPaid
def call(event)
# Charge credit card here
end
end
end
Rails.configuration.event_store.subscribe(Payments::OnOrderPaid.new, to: [OrderPaid])
Pros:
- Tight ActiveRecord integration
- Built-in event sourcing
Cons:
- Ruby-only (no cross-language events)
2. Karafka (Kafka Backbone)
Best for: Apps needing inter-service messaging.
# config/karafka.rb
class KarafkaApp < Karafka::App
setup do |config|
config.kafka = { 'bootstrap.servers': 'kafka:9092' }
end
routes.draw do
topic :orders_paid do
consumer Payments::OrdersPaidConsumer
end
end
end
# app/consumers/payments_consumer.rb
module Payments
class OrdersPaidConsumer < Karafka::BaseConsumer
def consume
params_batch.each do |order|
CreditCard.charge(order[:id])
end
end
end
end
Pros:
- Kafka’s durability/scaling
- Polyglot-friendly (Java services can listen)
Cons:
- Operational complexity (Kafka clusters)
3. Dry-Events (Pure Ruby)
Best for: Lightweight decoupling.
# config/initializers/events.rb
class AppEventBus
include Dry::Events::Publisher[:my_bus]
register_event("order.paid")
end
# Publish
AppEventBus.broadcast("order.paid", order_id: 123)
# Subscribe
AppEventBus.subscribe("order.paid") do |event|
Payments::CreditCardProcessor.call(event[:order_id])
end
Pros:
- No database/Kafka dependency
- Simple for small apps
Cons:
- No persistence (lost events on crash)
Critical Patterns
1. Event Sourcing
Store all state changes as events:
# Rebuild an order’s state from events
order_events = event_store.read.stream("Order$#{order.id}")
order = OrderProjection.rebuild(order_events)
Use when:
- Audit trails are critical (fintech, healthcare).
- You need time-travel debugging.
2. Idempotent Handlers
def call(event)
return if ProcessedEvent.exists?(event_id: event.event_id)
CreditCard.charge(event.data[:order_id])
ProcessedEvent.create!(event_id: event.event_id)
end
Prevents: Double charges when retrying failed events.
3. Schema Enforcement
Validate event shapes with JSON Schema:
class OrderPaid < RailsEventStore::Event
SCHEMA = {
"type": "object",
"required": ["order_id"],
"properties": {
"order_id": { "type": "string" }
}
}
def self.strict(data)
JSON::Validator.validate!(SCHEMA, data)
new(data)
end
end
When to Avoid EDA
🚫 Simple CRUD apps: Overkill for basic forms.
🚫 Low-latency needs: Event processing adds milliseconds.
🚫 No ops team: Kafka isn’t "just run it in Heroku".
Adoption Roadmap
- Start small: Replace one callback chain with events.
- Add RES: For event persistence.
- Scale out: Introduce Kafka when crossing service boundaries.
Pro tip: Use rails-event_store
’s TestClient
in CI:
Rails.configuration.event_store = RailsEventStore::Client.new(repository: RubyEventStore::InMemoryRepository.new)
"But Rails is request/response!"
So were PHP apps—until they weren’t.
EDA isn’t about chasing trends. It’s about writing systems that don’t snap when you add the 100th feature.
Have you tried EDA in Rails? Share your wins (or horror stories) below.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.