Make Ecto.Multi flow like water
Language / θ―θ¨: English | δΈζ
A DSL and Builder pattern wrapper for Ecto.Multi
that makes database transactions elegant, readable, and maintainable.
Working with Ecto.Multi
is powerful, but the syntax can become verbose and hard to follow. MultiFlow provides two elegant approaches:
Multi.new()
|> Multi.insert(:order, order_changeset)
|> Multi.run(:items, fn repo, %{order: order} ->
items
|> Enum.map(&create_item_changeset(&1, order))
|> Enum.reduce(Multi.new(), fn changeset, multi ->
Multi.insert(multi, {:item, changeset.changes.uuid}, changeset)
end)
|> repo.transaction()
|> case do
{:ok, items} -> {:ok, Map.values(items)}
{:error, _failed_operation, changeset, _changes} -> {:error, changeset}
end
end)
|> Multi.run(:delivery, fn repo, %{order: order} ->
create_delivery(order)
end)
|> Repo.transaction()
use MultiFlow
transaction do
step :order, insert(order_changeset)
step :items, fn %{order: order} ->
items
|> Enum.map(&create_item_changeset(&1, order))
|> insert_all()
end
step :delivery, fn %{order: order} ->
create_delivery(order)
end
end
MultiFlow.new()
|> add_step(:order, insert: order_changeset)
|> add_step(:items, fn %{order: order} ->
Enum.map(items, &create_item_changeset(&1, order))
end)
|> add_step(:delivery, &create_delivery/1, deps: [:order])
|> execute()
- π Flow naturally - Write transactions that read like prose
- π― DSL & Builder - Choose your style: declarative DSL or functional builder
- π Dependency tracking - Automatic step dependency management
- π‘οΈ Type safe - Full Dialyzer support
- π Readable - Code that explains itself
- π§ͺ Testable - Easy to test individual steps
- π Zero overhead - Compiles to raw
Ecto.Multi
Add multi_flow
to your list of dependencies in mix.exs
:
def deps do
[
{:multi_flow, "~> 1.0"}
]
end
defmodule MyApp.Orders do
use MultiFlow
def create_order(attrs, items_attrs) do
transaction do
# Step 1: Create order
step :order, insert(Order.changeset(%Order{}, attrs))
# Step 2: Create items (depends on order)
step :items, fn %{order: order} ->
items_attrs
|> Enum.map(&OrderItem.changeset(%OrderItem{}, &1, order))
|> insert_all()
end
# Step 3: Update inventory
step :update_inventory, fn %{items: items} ->
update_inventory(items)
end
# Step 4: Send notification
step :notify, fn %{order: order} ->
send_order_confirmation(order)
end
end
end
end
defmodule MyApp.Orders do
alias MultiFlow, as: MF
def create_order(attrs, items_attrs) do
MF.new()
|> MF.add_step(:order, insert: Order.changeset(%Order{}, attrs))
|> MF.add_step(:items, fn %{order: order} ->
Enum.map(items_attrs, &OrderItem.changeset(%OrderItem{}, &1, order))
end)
|> MF.add_step(:update_inventory, &update_inventory/1, deps: [:items])
|> MF.add_step(:notify, &send_order_confirmation/1, deps: [:order])
|> MF.execute()
end
end
Here's a complete sales order creation with error handling:
defmodule MyApp.Sales do
use MultiFlow
def create_sales_order(order_attrs, items_attrs, delivery_attrs) do
transaction do
# Validate customer
step :customer, fn _ ->
get_customer(order_attrs.customer_id)
end
# Create order
step :order, fn %{customer: customer} ->
order_attrs
|> Map.put(:customer_name, customer.name)
|> create_order()
end
# Create order items
step :items, fn %{order: order} ->
items_attrs
|> Enum.map(&prepare_item(&1, order))
|> create_items()
end
# Calculate totals
step :totals, fn %{items: items} ->
calculate_totals(items)
end
# Update order with totals
step :update_order, fn %{order: order, totals: totals} ->
update_order(order, totals)
end
# Create delivery
step :delivery, fn %{order: order} ->
create_delivery(order, delivery_attrs)
end
# Check inventory
step :check_inventory, fn %{items: items} ->
check_and_reserve_inventory(items)
end
# Send notifications
step :notify, fn %{order: order, customer: customer} ->
send_notifications(order, customer)
end
end
end
end
MultiFlow preserves Ecto.Multi
's excellent error handling:
case create_sales_order(attrs, items, delivery) do
{:ok, result} ->
# Success! All steps completed
%{
order: result.order,
items: result.items,
delivery: result.delivery
}
{:error, failed_step, changeset, changes_so_far} ->
# Handle error
Logger.error("Failed at step: #{failed_step}")
{:error, changeset}
end
- β You have complex multi-step transactions
- β You want more readable transaction code
- β You need to test transaction steps individually
- β You want better code organization
β οΈ You have very simple transactions (1-2 steps)β οΈ You need maximum performance (though difference is negligible)β οΈ Your team prefers explicit over implicit
MultiFlow compiles to raw Ecto.Multi
operations with zero runtime overhead. The abstractions are purely compile-time.
# This MultiFlow code:
transaction do
step :order, insert(changeset)
step :items, &create_items/1
end
# Compiles to:
Multi.new()
|> Multi.insert(:order, changeset)
|> Multi.run(:items, fn _repo, changes -> create_items(changes) end)
|> Repo.transaction()
Feature | Raw Ecto.Multi | MultiFlow DSL | MultiFlow Builder |
---|---|---|---|
Readability | βββ | βββββ | ββββ |
Verbosity | High | Low | Medium |
Testability | Good | Excellent | Excellent |
Learning Curve | Medium | Low | Low |
Flexibility | High | High | Very High |
Performance | Fast | Fast | Fast |
We welcome contributions! Please see CONTRIBUTING.md for details.
MIT License - see LICENSE for details.
Created by zven21
Inspired by:
- Ecto.Multi
- Commanded.Aggregate.Multi
- Railway Oriented Programming
Make your Ecto.Multi flow like water π