MultiFlow ๐ŸŒŠ

View Source

Make Ecto.Multi flow like water

Hex.pm Documentation License

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
end

Or (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"}
  ]
end

Quick 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
end

Builder 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
end

Real-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
end

Error 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}
end

Documentation

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

FeatureRaw Ecto.MultiMultiFlow DSLMultiFlow Builder
Readabilityโญโญโญโญโญโญโญโญโญโญโญโญ
VerbosityHighLowMedium
TestabilityGoodExcellentExcellent
Learning CurveMediumLowLow
FlexibilityHighHighVery High
PerformanceFastFastFast

Contributing

We welcome contributions! Please see CONTRIBUTING.md for details.

License

MIT License - see LICENSE for details.

Credits

Created by zven21

Inspired by:


Make your Ecto.Multi flow like water ๐ŸŒŠ