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