class TransferFundsService
def initialize(from_account:, to_account:, amount:)
@from_account = from_account
@to_account = to_account
@amount = amount
end
def call
ActiveRecord::Base.transaction do
# Lock rows to prevent concurrent modifications
from = Account.lock.find(@from_account.id)
to = Account.lock.find(@to_account.id)
raise InsufficientFundsError if from.balance < @amount
from.update!(balance: from.balance - @amount)
to.update!(balance: to.balance + @amount)
Transaction.create!(
from_account: from,
to_account: to,
amount: @amount,
status: 'completed'
)
end
Result.success
rescue InsufficientFundsError => e
Result.failure(error: 'INSUFFICIENT_FUNDS')
rescue StandardError => e
Rails.logger.error("TransferFundsService failed: #{e.message}")
Result.failure(error: 'TRANSFER_FAILED')
end
end
Transactions ensure that multiple database operations either all succeed or all fail together, preventing partial updates that leave data in inconsistent states. Rails provides ActiveRecord::Base.transaction which wraps a block of code in a database transaction. If any exception is raised within the block, all changes are rolled back automatically. This is critical for operations like transferring funds, creating orders with line items, or any workflow where related records must remain synchronized. I'm careful to keep transaction blocks focused and fast—long-running operations or external API calls inside transactions can cause lock contention and deadlocks. For complex workflows, I use database-level constraints as a second line of defense against invariant violations.