How to add actions after saving in Rails models? A new approach...
Every developer knows how frustrating race condition issues can be, especially when it comes to background tasks. How many times have you had a job try to find a record in the database that... hasn't been fully saved yet? Today, we'll take a look at a powerful new tool in the Rails arsenal that elegantly solves this problem: ActiveRecord.after_all_transactions_commit
.
What is after_all_transactions_commit?
In short, ActiveRecord.after_all_transactions_commit
is a callback that allows you to execute specific code only after all open database transactions have been successfully completed and saved. Regardless of whether your code is inside or outside a transaction block, you can be sure that the action will only be executed once the data has been safely committed to the database. This is ideal for queuing background tasks, sending notifications, or triggering webhooks—anywhere where data consistency is critical.
Use inside a transaction block
Let's look at a classic example: user registration confirmation. We want to update the user's status and send them a confirmation email. It is crucial that the email is sent only after the change in the database has been irreversibly saved.
def confirm_user(user)
User.transaction do
user.update(confirmed: true)
# This block will only be executed after the transaction is successfully committed.
ActiveRecord.after_all_transactions_commit do
UserConfirmationMailer.with(user: user).deliver_later
end
end
end
In this code, the user.update
operation is wrapped in a User.transaction
block. The UserConfirmationMailer
mailer will only be added to the ActiveJob queue after this transaction has been successfully completed. If for some reason the transaction is rolled back, the email will never be sent. Simple and reliable.
Use outside the transaction block
The power of after_all_transactions_commit
lies in its flexibility. You don't need to explicitly define a transaction block to use it. Rails is smart enough to know when write operations (such as create
or update
) open an implicit transaction.
Consider the example of creating a new user and running a background task that creates a profile for them.
def create_user(user_params)
user = User.create(user_params)
# The job will be enqueued only after the user record is persisted.
ActiveRecord.after_all_transactions_commit do
CreateUserProfileJob.perform_later(user)
end
end
Similarly, when updating an order, we first save the changes and only then run the potentially costly operation of recalculating the summary.
def update_order(order, order_params)
order.update(order_params)
# The summary update will run after the order changes are saved.
ActiveRecord.after_all_transactions_commit do
order.update_order_summary
end
end
It is also worth mentioning that ActiveJob now defaults to deferring task queuing until the transaction is complete. If the transaction is rolled back, the task will be rejected. This built-in behavior further protects us from RecordNotFound
errors in background tasks.
Why is this so important?
The new after_all_transactions_commit
callback is more than just a syntactic addition. It is a fundamental improvement to the reliability of Rails applications.
First, we gain reliability - We are guaranteed that dependent actions will only execute after the data has been successfully saved, which eliminates an entire class of errors.
Second, better separation of logic - We can separate transaction management from business logic in models, leading to cleaner and more modular code.
Third, flexibility - The callback works both inside and outside transaction blocks, making it a universal tool.
Summary
Implementing this feature significantly enhances the robustness of background task processing and other asynchronous operations, making our applications more stable and easier to maintain.
Happy queue-ing!