Architecture

Batches vs Pipelines

When to Use Each

Scenario Pipeline Batch
Simple data transformation Yes No
Single value passed between steps Yes No
Multiple parameters needed No Yes
Conditional logic between steps No Yes
Different error handling per step No Yes
Steps can run independently No Yes
Linear A → B → C flow Yes No

Pipelines

Use pipelines when data flows linearly through a series of transformations, where each step receives the output of the previous step.

Characteristics

  • Single value passed through the chain
  • Each step transforms and returns data
  • All steps always run in sequence
  • Exceptions bubble up and halt the pipeline
  • Steps are simple, focused transformations

Good Use Cases

  • Data transformation (convert formats, units, types)
  • Request/response modification
  • Text processing (sanitize, format, validate)
  • Building query conditions

Example

<?php

declare(strict_types=1);

namespace App\Services;

use App\Actions\Units\ConvertFeetToInches;
use App\Actions\Units\ConvertInchesToCentimeters;
use App\Actions\Units\ConvertCentimetersToMeters;
use Illuminate\Pipeline\Pipeline;

final class UnitConverter
{
    public function feetToMeters(float $feet): float
    {
        return app(Pipeline::class)
            ->send($feet)
            ->through([
                fn (float $feet, $next) => $next(
                    app(ConvertFeetToInches::class)->execute($feet)
                ),
                fn (float $inches, $next) => $next(
                    app(ConvertInchesToCentimeters::class)->execute($inches)
                ),
                fn (float $cm, $next) => $next(
                    app(ConvertCentimetersToMeters::class)->execute($cm)
                ),
            ])
            ->thenReturn();
    }
}

With Invokable Classes

For cleaner syntax, make pipeline steps invokable:

<?php

declare(strict_types=1);

namespace App\Pipes;

use Closure;

final class ConvertFeetToInches
{
    public function handle(float $feet, Closure $next): mixed
    {
        $inches = $feet * 12;

        return $next($inches);
    }
}
// Usage
app(Pipeline::class)
    ->send($feet)
    ->through([
        ConvertFeetToInches::class,
        ConvertInchesToCentimeters::class,
        ConvertCentimetersToMeters::class,
    ])
    ->thenReturn();

Batches

Use batches when orchestrating multiple actions that may have different inputs, conditional logic, or independent error handling.

Characteristics

  • Multiple parameters passed to different steps
  • Conditional execution based on previous results
  • Per-step error handling and recovery
  • Steps may call external services
  • Complex workflows with branching logic

Good Use Cases

  • User onboarding (create accounts across multiple services)
  • Order processing (payment, inventory, shipping, notifications)
  • Data sync across external systems
  • Multi-step operations with rollback needs

Example

<?php

declare(strict_types=1);

namespace App\Batches;

use App\Actions\Users\CreateUser;
use App\Actions\GitHub\CheckGitHubAccount;
use App\Actions\GitHub\SetGitHubPermissions;
use App\Actions\Slack\SyncUserToSlack;
use App\Actions\Slack\AddUserToChannels;
use App\Actions\Email\SendWelcomeEmail;
use App\Data\UserData;
use App\Models\User;
use App\Notifications\GitHubSyncFailed;
use App\Notifications\SlackSyncFailed;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
use Throwable;

final class CreateUserBatch
{
    public function __construct(
        private CreateUser $createUser,
        private CheckGitHubAccount $checkGitHubAccount,
        private SetGitHubPermissions $setGitHubPermissions,
        private SyncUserToSlack $syncUserToSlack,
        private AddUserToChannels $addUserToChannels,
        private SendWelcomeEmail $sendWelcomeEmail,
    ) {}

    /**
     * @param array<string> $slackChannels
     */
    public function handle(UserData $data, array $slackChannels): User
    {
        $user = $this->createUser->execute($data);

        $this->setupGitHub($user);
        $this->setupSlack($user, $slackChannels);
        $this->sendWelcomeEmail->execute($user);

        return $user;
    }

    private function setupGitHub(User $user): void
    {
        if ($this->checkGitHubAccount->execute($user)) {
            return;
        }

        try {
            $this->setGitHubPermissions->execute($user);
        } catch (Throwable $e) {
            Log::error('GitHub sync failed', [
                'user_id' => $user->id,
                'error' => $e->getMessage(),
            ]);
            Notification::route('mail', config('services.github.admin_email'))
                ->notify(new GitHubSyncFailed($user, $e));
        }
    }

    /**
     * @param array<string> $channels
     */
    private function setupSlack(User $user, array $channels): void
    {
        try {
            $this->syncUserToSlack->execute($user);
            $this->addUserToChannels->execute($user, $channels);
        } catch (Throwable $e) {
            Log::error('Slack sync failed', [
                'user_id' => $user->id,
                'error' => $e->getMessage(),
            ]);
            Notification::route('mail', config('services.slack.admin_email'))
                ->notify(new SlackSyncFailed($user, $e));
        }
    }
}

Key Differences

Pipeline

// Data flows: A → B → C
// Each step receives previous output
$result = Pipeline::send($input)
    ->through([StepA::class, StepB::class, StepC::class])
    ->thenReturn();

Batch

// Orchestrated steps with shared context
// Each step can receive different inputs
$user = $this->createUser->execute($data);
$account = $this->createAccount->execute($user, $plan);
$this->sendWelcome->execute($user, $account);

File Organization

app/
├── Batches/           # Batch orchestrators
│   ├── CreateUserBatch.php
│   └── ProcessOrderBatch.php
├── Pipes/             # Pipeline step classes
│   ├── SanitizeInput.php
│   └── FormatOutput.php
└── Actions/           # Reusable single actions
    └── Users/
        └── CreateUser.php