Spot the Tax · Card 19 of 20
When the controller knows about every downstream concern
Why a create action that fires payment, inventory, email, analytics, and Slack inline becomes a coordination nightmare.
The code
What will this cost you in six months?
class OrdersController < ApplicationController
def create
order = Order.create!(order_params)
PaymentService.charge(order)
InventoryService.reserve(order)
OrderConfirmationMailer.with(order: order).deliver_later
AnalyticsService.track(order, "order_placed")
SlackNotifier.notify_sales(order) if order.total_cents > 1_000_00
AffiliateService.credit(order) if order.referrer_code.present?
redirect_to order
end
end The problem
Every concern that runs after an order is placed lives in this controller. Adding loyalty points means editing this controller. Adding fraud screening means editing this controller. Every team that wants to do something when an order is placed has to come and add a line. The action becomes a junk drawer of unrelated calls, and any failure in one of them risks taking down the others or leaving the system in an inconsistent state.
Take a moment. Before revealing, ask yourself how you'd let other parts of the app react to an order being placed without the controller having to know about each one.
The solution
Have the controller announce that an order was placed, and let each interested concern subscribe to that announcement. The controller now does one thing — place the order and emit the event. Adding loyalty points becomes a new subscriber, with no edits to the controller.
- The action does one thing: place the order
- New side effects are new listeners, not edits to the controller
- Each listener can be retried, logged, and tested on its own
class OrdersController < ApplicationController
def create
order = Order.create!(order_params)
OrderEvents.publish(:placed, order)
redirect_to order
end
end
# Each concern lives where it belongs:
OrderEvents.subscribe(:placed) { |order| PaymentJob.perform_later(order.id) }
OrderEvents.subscribe(:placed) { |order| InventoryReservationJob.perform_later(order.id) }
OrderEvents.subscribe(:placed) { |order| OrderConfirmationMailer.with(order: order).deliver_later }
# Adding loyalty points is one new subscriber, no controller edits. The principle at play — Domain events
When something significant happens in your domain — an order is placed, a user signs up, a payment fails — the code that triggers it shouldn't have to know everyone who cares. The trigger announces "this happened"; everyone who's interested arranges to react on their own. That's a domain event.
The benefit is that adding a new reaction doesn't require touching the trigger. The team that wants to send a Slack alert when high-value orders come in can register their own subscriber, in their own file, without coordinating with the team that owns the orders controller. The trigger keeps doing exactly one thing: triggering. Everything downstream is somebody else's responsibility.
Rails has a built-in version of this with ActiveSupport::Notifications, and gems like wisper exist for richer pub/sub. The mechanism matters less than the shift in mindset: the action that does the thing is not responsible for the things that happen because of it.