# Use `Ecto.Multi` for complex transactions in Phoenix contexts

Manual transaction management with nested operations becomes unreadable and error-prone. `Ecto.Multi` provides named operations, automatic rollback, and clear transaction composition.

```elixir
# ❌ MANUAL TRANSACTIONS - Hard to read and maintain
defmodule Accounts do
  def register_user(attrs) do
    Repo.transaction(fn ->
      with {:ok, user} <- create_user(attrs),
           {:ok, profile} <- create_profile(user),
           {:ok, _settings} <- create_default_settings(user),
           {:ok, _audit} <- log_registration(user) do
        user
      else
        {:error, changeset} ->
          Repo.rollback(changeset)
      end
    end)
  end
end

# ✅ ECTO.MULTI - Self-documenting transaction steps
defmodule Accounts do
  def register_user(attrs) do
    Multi.new()
    |> Multi.insert(:user, User.changeset(%User{}, attrs))
    |> Multi.insert(:profile, fn %{user: user} ->
      Profile.changeset(%Profile{user_id: user.id}, %{})
    end)
    |> Multi.insert(:settings, fn %{user: user} ->
      Settings.default_changeset(user)
    end)
    |> Multi.insert(:audit, fn %{user: user} ->
      AuditLog.registration_entry(user)
    end)
    |> Repo.transaction()
  end
end
```

Each operation in the `Multi` pipeline is named and can reference results from previous steps. If any operation fails, the entire transaction rolls back automatically.

## Access transaction results by name

`Multi` returns a map with all operation results, making it easy to work with multiple related records:

```elixir
case Accounts.register_user(user_params) do
  {:ok, %{user: user, profile: profile, settings: settings}} ->
    send_welcome_email(user)
    {:ok, user}

  {:error, :user, changeset, _changes_so_far} ->
    {:error, "Failed to create user", changeset}

  {:error, :profile, changeset, _changes_so_far} ->
    {:error, "Failed to create profile", changeset}

  {:error, failed_operation, failed_value, changes_so_far} ->
    {:error, "Transaction failed at #{failed_operation}"}
end
```

## Conditional operations with `Multi.run/3`

Use `Multi.run/3` for operations that need custom logic or external service calls:

```elixir
defmodule Billing do
  def create_subscription(user, plan_id) do
    Multi.new()
    |> Multi.run(:validate_plan, fn _repo, _changes ->
      case Plans.get(plan_id) do
        nil -> {:error, :invalid_plan}
        plan -> {:ok, plan}
      end
    end)
    |> Multi.run(:charge_payment, fn _repo, %{validate_plan: plan} ->
      StripeAPI.charge_customer(user, plan.price)
    end)
    |> Multi.insert(:subscription, fn %{validate_plan: plan} ->
      Subscription.changeset(%Subscription{}, %{
        user_id: user.id,
        plan_id: plan.id,
        status: :active
      })
    end)
    |> Multi.update(:user, fn %{subscription: sub} ->
      User.changeset(user, %{subscription_id: sub.id})
    end)
    |> Repo.transaction()
  end
end
```

If `StripeAPI.charge_customer/2` fails, the entire transaction rolls back - no subscription record is created, and the user isn't updated.

## Reusable transaction components

Build complex transactions from smaller, reusable pieces:

```elixir
defmodule OrderProcessing do
  def create_order_multi(user, items) do
    Multi.new()
    |> Multi.insert(:order, Order.new_changeset(user))
    |> Multi.merge(fn %{order: order} ->
      add_order_items_multi(order, items)
    end)
  end

  defp add_order_items_multi(order, items) do
    items
    |> Enum.with_index()
    |> Enum.reduce(Multi.new(), fn {item, index}, multi ->
      Multi.insert(multi, {:item, index}, OrderItem.changeset(order, item))
    end)
  end

  def complete_order(order_id) do
    Multi.new()
    |> Multi.update(:order, fn _ ->
      order = Repo.get!(Order, order_id)
      Order.complete_changeset(order)
    end)
    |> Multi.merge(fn %{order: order} ->
      charge_payment_multi(order)
    end)
    |> Multi.merge(fn %{order: order} ->
      send_confirmation_multi(order)
    end)
    |> Repo.transaction()
  end

  defp charge_payment_multi(order), do: # ...
  defp send_confirmation_multi(order), do: # ...
end
```

## Debugging with named operations

When a transaction fails, you know exactly which step caused the problem:

```elixir
case Repo.transaction(multi) do
  {:ok, changes} ->
    Logger.info("Transaction completed: #{inspect(Map.keys(changes))}")
    {:ok, changes}

  {:error, failed_operation, failed_value, changes_so_far} ->
    Logger.error("""
    Transaction failed at: #{failed_operation}
    Error: #{inspect(failed_value)}
    Completed steps: #{inspect(Map.keys(changes_so_far))}
    """)
    {:error, failed_operation, failed_value}
end
```

Use `Ecto.Multi` whenever you have multiple database operations that must succeed or fail together. Your transaction logic becomes testable, composable, and self-documenting.


---

Created by: almirsarajcic
Date: December 10, 2025
URL: https://elixirdrops.net/d/NfbMUJPX
