Events vs Commands: Untangling Event-Driven Architecture in a Rails System
Two very different contracts hide behind the same "event-driven" label. Getting the naming right decides how the rest of the architecture behaves.
Publié le 13 octobre 2024 · 5 min de lecture
"Event-driven" gets used as a label for two architectures that behave very differently under load and under failure. Before naming a queue or a topic, it's worth being precise about which one you're actually building.
A command tells, an event informs
A command names an action you want performed and expects it to happen: CreateInvoice, CancelSubscription. It's addressed to exactly one recipient, who is allowed to refuse it. An event describes something that already happened, in the past tense: OrderPlaced, InvoicePaid. It's addressed to nobody in particular; the publisher doesn't know, and shouldn't care, who's listening.
Where teams get burned
The most common mistake is publishing something named like an event but shaped like a command, "SendWelcomeEmail" for instance, then being surprised when someone else's service acts on it in a way that couples the two teams' release schedules together. If a consumer's behavior is required for your business process to be correct, you're issuing a command and should treat the delivery guarantee accordingly. If the process is correct with or without that consumer reacting, it's genuinely an event.
Rails, ActiveSupport::Notifications and where it stops
Inside a single Rails process, ActiveSupport::Notifications gives you the event pattern for free: publish once, let zero or more subscribers react, none of them block each other or the publisher.
ActiveSupport::Notifications.instrument("order.placed", order_id: order.id)
ActiveSupport::Notifications.subscribe("order.placed") do |*, payload|
LoyaltyPoints.credit(payload[:order_id])
end
That pattern works beautifully in-process and falls apart the moment a subscriber needs to survive a process restart or live in a different service. That's the point where you reach for a durable transport, and where the outbox pattern earns its keep.
Naming is the design decision
In practice, the architecture review that matters most isn't "Kafka or RabbitMQ." It's making every team write down, per message, whether they're issuing a command or announcing a fact. Everything else, retries, ordering, replay, follows from that answer.
Articles liés
The Outbox Pattern in Rails: How to Publish Events Without Losing Data
The gap between saving a record and publishing the message that announces it is where events quietly disappear. Here is how to close it.
Idempotent by Design: Making Your Sidekiq Jobs Safe to Retry
Sidekiq guarantees at-least-once delivery, not exactly-once. Any job that is not safe to run twice is a bug waiting for the right overlap.