MultiFlow ๐
View SourceMake Ecto.Multi flow like water
A DSL and Builder pattern wrapper for Ecto.Multi that makes database transactions elegant, readable, and maintainable.
Why MultiFlow?
Working with Ecto.Multi is powerful, but the syntax can become verbose and hard to follow. MultiFlow provides two elegant approaches:
Before (Raw Ecto.Multi)
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()After (MultiFlow DSL)
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
endOr (MultiFlow Builder)
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()Features
- ๐ 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
Installation
Add multi_flow to your list of dependencies in mix.exs:
def deps do
[
{:multi_flow, "~> 1.0"}
]
endQuick Start
DSL Style
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
endBuilder Style
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
endReal-World Example
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
endError Handling
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}
endDocumentation
When to Use MultiFlow?
Use MultiFlow when:
- โ You have complex multi-step transactions
- โ You want more readable transaction code
- โ You need to test transaction steps individually
- โ You want better code organization
Stick with raw Ecto.Multi when:
- โ ๏ธ You have very simple transactions (1-2 steps)
- โ ๏ธ You need maximum performance (though difference is negligible)
- โ ๏ธ Your team prefers explicit over implicit
Performance
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()Comparison
| 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 |
Contributing
We welcome contributions! Please see CONTRIBUTING.md for details.
License
MIT License - see LICENSE for details.
Credits
Created by zven21
Inspired by:
- Ecto.Multi
- Commanded.Aggregate.Multi
- Railway Oriented Programming
Make your Ecto.Multi flow like water ๐