documentation/tutorials/04-composition.md

# Building Complex Workflows with Composition

In this tutorial, you'll learn how to build large, maintainable workflows by composing smaller reactors together. This is essential for managing complexity in real-world applications.

## What you'll build

A multi-stage e-commerce order processing system that:
1. **User Management** - Validates and enriches user data
2. **Inventory Management** - Checks and reserves product inventory  
3. **Payment Processing** - Handles payment authorization and capture
4. **Order Fulfillment** - Coordinates shipping and notifications
5. **Master Orchestrator** - Composes all sub-workflows together

## You'll learn

- How to break complex workflows into composable reactors
- When to use composition vs building one large reactor
- How to pass data between composed reactors
- Error handling and rollback across composed workflows
- Testing strategies for composed systems

## Prerequisites

- Complete the [Getting Started tutorial](01-getting-started.md)
- Complete the [Error Handling tutorial](02-error-handling.md)  
- Complete the [Async Workflows tutorial](03-async-workflows.md)

## Step 1: Set up the project

If you don't have a project from the previous tutorials:

```bash
mix igniter.new reactor_tutorial --install reactor
cd reactor_tutorial
```

## Step 2: Understanding Reactor composition

Reactor composition allows you to:

**Break down complexity** - Instead of one massive reactor, create focused sub-reactors:
```elixir
# Use composition:
defmodule OrderProcessor do
  use Reactor
  
  compose :user_management, UserManagementReactor
  compose :inventory_check, InventoryReactor  
  compose :payment_processing, PaymentReactor
  compose :fulfillment, FulfillmentReactor
end
```

**Enable reusability** - Sub-reactors can be used in multiple contexts:
```elixir
compose :validate_buyer, UserManagementReactor
compose :validate_seller, UserManagementReactor  
```

**Improve testability** - Test each sub-reactor independently.

## Step 3: Create simple domain reactors

Let's start by building focused reactors for each domain. Create `lib/user_validation_reactor.ex`:

```elixir
defmodule UserValidationReactor do
  use Reactor

  input :user_id

  step :fetch_user do
    argument :user_id, input(:user_id)
    
    run fn %{user_id: user_id}, _context ->
      {:ok, %{
        id: user_id,
        name: "User #{user_id}",
        email: "user#{user_id}@example.com",
        active: true
      }}
    end
  end

  step :validate_user do
    argument :user, result(:fetch_user)
    
    run fn %{user: user}, _context ->
      if user.active do
        {:ok, user}
      else
        {:error, "User is not active"}
      end
    end
  end

  return :validate_user
end
```

Create `lib/inventory_reactor.ex`:

```elixir
defmodule InventoryReactor do
  use Reactor

  input :product_id
  input :quantity

  step :check_availability do
    argument :product_id, input(:product_id)
    argument :quantity, input(:quantity)
    
    run fn %{product_id: product_id, quantity: quantity}, _context ->
      available = 50
      
      if quantity <= available do
        {:ok, %{product_id: product_id, available: available}}
      else
        {:error, "Not enough inventory"}
      end
    end
  end

  step :reserve_items do
    argument :availability, result(:check_availability)
    argument :quantity, input(:quantity)
    
    run fn %{availability: avail, quantity: qty}, _context ->
      reservation = %{
        product_id: avail.product_id,
        quantity: qty,
        reserved_at: DateTime.utc_now()
      }
      {:ok, reservation}
    end
  end

  return :reserve_items
end
```

Create `lib/payment_reactor.ex`:

```elixir
defmodule PaymentReactor do
  use Reactor

  input :user_id
  input :amount

  step :validate_payment do
    argument :user_id, input(:user_id)
    argument :amount, input(:amount)
    
    run fn %{user_id: user_id, amount: amount}, _context ->
      if amount > 0 and amount < 10000 do
        {:ok, %{user_id: user_id, amount: amount, valid: true}}
      else
        {:error, "Invalid payment amount"}
      end
    end
  end

  step :process_payment do
    argument :payment_info, result(:validate_payment)
    
    run fn %{payment_info: info}, _context ->
      payment = %{
        payment_id: "pay_#{:rand.uniform(1000)}",
        user_id: info.user_id,
        amount: info.amount,
        status: :completed,
        processed_at: DateTime.utc_now()
      }
      {:ok, payment}
    end
  end

  return :process_payment
end
```

## Step 4: Create the master orchestrator

Now create the main reactor that composes all sub-reactors. Create `lib/order_processing_reactor.ex`:

```elixir
defmodule OrderProcessingReactor do
  use Reactor

  input :user_id
  input :product_id
  input :quantity
  input :amount

  # Step 1: Validate user
  compose :user_validation, UserValidationReactor do
    argument :user_id, input(:user_id)
  end

  # Step 2: Check inventory (can run in parallel with payment)
  compose :inventory_check, InventoryReactor do
    argument :product_id, input(:product_id)
    argument :quantity, input(:quantity)
  end

  # Step 3: Process payment
  compose :payment_processing, PaymentReactor do
    argument :user_id, input(:user_id)
    argument :amount, input(:amount)
  end

  # Step 4: Create final order (depends on all previous steps)
  step :create_order do
    argument :user, result(:user_validation)
    argument :reservation, result(:inventory_check)
    argument :payment, result(:payment_processing)
    
    run fn %{user: user, reservation: res, payment: pay}, _context ->
      order = %{
        order_id: "order_#{:rand.uniform(1000)}",
        user: user,
        product_id: res.product_id,
        quantity: res.quantity,
        payment_id: pay.payment_id,
        total: pay.amount,
        created_at: DateTime.utc_now()
      }
      
      {:ok, order}
    end
  end

  return :create_order
end
```

## Step 5: Test the composition

Let's test our composed reactor:

```bash
iex -S mix
```

```elixir
# Test the composed order processing
{:ok, order} = Reactor.run(OrderProcessingReactor, %{
  user_id: 123,
  product_id: 456,
  quantity: 2,
  amount: 99.99
})

IO.inspect(order.order_id)
# Should output something like "order_123"

# Test individual reactors too
{:ok, user} = Reactor.run(UserValidationReactor, %{user_id: 123})
IO.inspect(user.name)
# Should output "User 123"
```

## What you learned

You now understand Reactor composition:

- **Composition over monoliths** - Many small reactors are easier to manage than one large one
- **Clear interfaces** - Define clear inputs and outputs for each sub-reactor
- **Independent testing** - Test each reactor in isolation before testing compositions
- **Error boundaries** - Failures in sub-reactors can be handled by the parent reactor
- **Reusability** - Well-designed sub-reactors can be used in multiple contexts

### Design guidelines:

- **Single responsibility** - Each reactor should have one clear purpose
- **Loose coupling** - Minimise dependencies between reactors
- **High cohesion** - Related steps belong in the same reactor

## What's next

Now that you understand composition, you're ready for advanced patterns:

- **[Recursive Execution](05-recursive-execution.md)** - Handle iterative and recursive workflows
- **[Testing Strategies](documentation/how-to/testing-strategies.md)** - Comprehensive testing approaches
- **[Performance Optimization](documentation/how-to/performance-optimization.md)** - Advanced performance techniques

## Common issues

**Sub-reactor outputs don't match expectations**: Use clear interface contracts and validate inputs/outputs in tests

**Error handling doesn't work across boundaries**: Ensure compensation and undo are implemented in the appropriate reactor layer

**Composed reactors are hard to debug**: Test each sub-reactor individually and use descriptive logging

Happy building modular workflows! 🧩