Since these background jobs have lots of coordination to do that doesn't fit into traditional ActiveRecord model classes, you can break it down into smaller and smaller chunks of related logic until you get manageable chunks of code.
An example of breaking this down would be:
- Job: The high level thing you are trying to accomplish
- Tasks: Smaller chunks of work that still require coordination, but do so in a more focused manner
- Services: Classes that make web service calls, etc
- Entity classes: Your ActiveRecord classes
An example of breaking this down would be this (naively) implemented order processing background job for an e-commerce application.
The job (OrderProcessorJob) gets the pending orders, submits payment, ships the inventory and completes the order, saving it to the database. It involves web service calls, database calls, and a task (ProcessOrderTask) which has the knowledge of processing a single order.
The OrderProcessorJob class:
pending_orders.each do |pending_order|
order_to_process = Order.find pending_order.order_id
process_order_task.process order_to_process pending_order
# Log that we got an order we couldn't process
rescue Exception => e1
# Log error
rescue Exception => e2
# Log error, mark this job as "failed"
@order_service ||= OrderService.new
@process_order_task ||= ProcessOrderTask.new
OrderProcessorJob has the following responsibilities:
Get pending orders from a web service
Order from the database and pass the pending order, and the order to process to the
High level error handling ensuring the background job doesn't crash, and it can continue processing orders in case of problems with individual orders
The ProcessOrderTask class:
def process(order_to_process, pending_order)
rescue BillingException => billing_error
# Log error, mark order as failed due to billing reasons
rescue InventoryException => inventory_error
# Log error, mark order as failed due to inventory issues
@billing_service ||= BillingService.new
@inventory_service ||= InventoryService.new
The responsibilities of the
ProcessOrderTask class include:
- Calling the billing web service to ensure payment is confirmed
- Calling the inventory service to update the inventory to reflect that the order is complete
- Mark the order as completed and save it to the database
- Error handling in case of billing or inventory problems
The big advantage of this approach is you put most of the logic in other classes, which could be reused outside the scope of a background job. When each class has a more focused responsibility, you make it easier to test, because there is less code to test, and the numbers of dependencies for each class are reduced.