Spot the Tax · Card 6 of 20
One class, one reason to change
Why a class that handles payment, inventory, email, analytics, and audit logs becomes the file no one wants to touch.
The code
What will this cost you in six months?
class OrderProcessor
def process(order)
return false unless valid?(order)
charge_card(order)
update_inventory(order)
send_confirmation_email(order)
notify_warehouse(order)
update_analytics(order)
log_to_audit(order)
true
end
# ... 200 lines of private methods
end The problem
This class has six different reasons it might need to change in the future, all rolled into one file. Whenever the payment provider changes, you have to touch this class. Whenever the inventory system changes, same thing. The same goes for emails, the warehouse API, the analytics events, and the audit log format. Every team in the company eventually ends up working in this file at some point, and every change to one piece carries a real risk of accidentally breaking something else.
Take a moment. Before revealing, think about how you'd split this class. What would each new piece be responsible for, and what does OrderProcessor end up doing?
The solution
Split each responsibility into its own small class. OrderProcessor stays as the orchestrator that knows the order in which steps run, but each of the six steps becomes a focused object that knows how to do its piece and nothing else. A change to the payment logic now only touches the payment class, and a change to the email template only touches the email class.
- Each piece can be tested without setting up the others
- A change to one component touches one file
- The orchestrator's flow is readable in 10 lines
class OrderProcessor
def initialize(order)
@order = order
end
def process
OrderValidator.new(@order).check!
OrderCharger.new(@order).charge!
InventoryUpdater.new(@order).decrement!
OrderConfirmationMailer.with(order: @order).deliver_later
WarehouseNotification.new(@order).enqueue
OrderAuditLogger.log(@order)
end
end The principle at play — Single responsibility
Every class in your codebase has a set of reasons it might need to change in the future. Maybe a downstream API changes its contract. Maybe the business rules around something get updated. Maybe you switch vendors. The Single Responsibility Principle says a class should ideally have only one of those reasons — one axis along which it might evolve.
When a class ends up with several unrelated responsibilities, every change to one of them risks accidentally affecting the others. The tests get harder to write because you have to stub all the unrelated collaborators. The file gets longer and longer, and reasoning about its behavior gets harder because you have to juggle all the different concerns in your head at once.
Splitting by responsibility doesn't mean breaking everything down into one-method classes. It means each class should be clearly answering one question. The orchestrator's job is to know what runs and in what order. Each step's job is to do its piece. The orchestrator doesn't care how the email is built, and the email class doesn't care about the warehouse.