I’m building a Phoenix server that handles financial transactions, It’s very simple it creates an account, creates a user, and processes transactions between users. The question that I have is related to concurrency, to give more context the code below creates a transaction:
def create_transaction(%{sender_id: sender_id, recipient_id: recipient_id, amount: amount}) do
sender_update_query =
from Account,
where: [id: ^sender_id],
update: [inc: [balance: ^(-amount)]]
recipient_update_query =
from Account,
where: [id: ^recipient_id],
update: [inc: [balance: ^(+amount)]]
Multi.new()
|> Multi.run(:retrieved_accounts, &retrieved_accounts(&1, &2, recipient_id, sender_id))
|> Multi.run(:check_sender_funds, &check_sender_funds(&1, &2, amount))
|> Multi.update_all(:recipient_update_query, recipient_update_query, [])
|> Multi.run(:check_recipient_update_query, &check_recipient_update_query(&1, &2))
|> Multi.update_all(:sender_update_query, sender_update_query, [])
|> Multi.run(:check_sender_update_query, &check_sender_update_query(&1, &2))
|> Multi.insert(:insert_transaction, %Transaction{
sender_id: sender_id,
recipient_id: recipient_id,
amount: amount
})
|> Repo.transaction()
|> handle_multi()
end
defp retrieved_accounts(_repo, _changes, recipient_id, sender_id) do
id_list = [sender_id, recipient_id]
Accounts.get_sender_and_recipient_accounts(id_list, sender_id, recipient_id)
end
defp check_sender_funds(_repo, %{retrieved_accounts: [sender_account, _]}, amount) do
if sender_account.balance - amount >= 0,
do: {:ok, nil},
else: {:error, :insufficient_funds}
end
defp check_recipient_update_query(
_repo,
%{recipient_update_query: {1, _}}
) do
{:ok, nil}
end
defp check_recipient_update_query(
_repo,
%{recipient_update_query: {_, _}}
) do
{:error, :failed_transfer}
end
defp check_sender_update_query(
_repo,
%{sender_update_query: {1, _}}
) do
{:ok, nil}
end
defp check_sender_update_query(_repo, %{sender_update_query: {_, _}}) do
{:error, :failed_transfer}
end
defp handle_multi({:ok, %{insert_transaction: transaction}}), do: {:ok, transaction}
defp handle_multi({:ok, %{update_transaction: transaction}}), do: {:ok, transaction}
defp handle_multi({:error, _id, error_or_changeset, _multi}), do: {:error, error_or_changeset}
My question is if this function will handle cases where multiple transactions can happen at the same time.
Another thing that I should mention is that I also have a function that charges back the transaction, which means that the balance can be modified by another function. I don’t know if Ecto can handle this kind of operation or if I should try to use something like a Genserver to orchestrate the transactions. Thoughts?