Laravel Pipeline Pattern: Beyond Middleware | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    The Pipeline Pattern in Laravel: Building Custom Pipelines Beyond Middleware        On this page       1. [  The Pipeline Pattern in Laravel: Beyond Middleware ](#the-pipeline-pattern-in-laravel-beyond-middleware)
2. [  What the Pipeline Class Actually Does ](#what-the-pipeline-class-actually-does)
3. [  Defining Pipe Classes ](#defining-pipe-classes)
4. [  Using a Custom Method Name ](#using-a-custom-method-name)
5. [  Short-Circuiting with Exceptions vs. Early Returns ](#short-circuiting-with-exceptions-vs-early-returns)
6. [  Resolving Pipes from the Container ](#resolving-pipes-from-the-container)
7. [  Testing Individual Pipes in Isolation ](#testing-individual-pipes-in-isolation)
8. [  When to Reach for a Pipeline ](#when-to-reach-for-a-pipeline)
9. [  Takeaways ](#takeaways)

  ![The Pipeline Pattern in Laravel: Building Custom Pipelines Beyond Middleware](https://cdn.msaied.com/172/a5dea5e84a6eab22d5d7a76869aaecb4.png)

  #laravel   #design-patterns   #pipeline   #clean-architecture  

 The Pipeline Pattern in Laravel: Building Custom Pipelines Beyond Middleware 
==============================================================================

     14 Jun 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

  9 sections  

1. [  01   The Pipeline Pattern in Laravel: Beyond Middleware  ](#the-pipeline-pattern-in-laravel-beyond-middleware)
2. [  02   What the Pipeline Class Actually Does  ](#what-the-pipeline-class-actually-does)
3. [  03   Defining Pipe Classes  ](#defining-pipe-classes)
4. [  04   Using a Custom Method Name  ](#using-a-custom-method-name)
5. [  05   Short-Circuiting with Exceptions vs. Early Returns  ](#short-circuiting-with-exceptions-vs-early-returns)
6. [  06   Resolving Pipes from the Container  ](#resolving-pipes-from-the-container)
7. [  07   Testing Individual Pipes in Isolation  ](#testing-individual-pipes-in-isolation)
8. [  08   When to Reach for a Pipeline  ](#when-to-reach-for-a-pipeline)
9. [  09   Takeaways  ](#takeaways)

       The Pipeline Pattern in Laravel: Beyond Middleware
--------------------------------------------------

Most Laravel developers know `Pipeline` as the invisible machinery that runs HTTP middleware. Few reach for it when solving domain problems — and that's a missed opportunity. The `Illuminate\Pipeline\Pipeline` class is a first-class, general-purpose tool for building ordered, composable processing chains over any payload.

### What the Pipeline Class Actually Does

At its core, `Pipeline` wraps a passable value through an ordered list of stages (pipes), each of which can transform the value or short-circuit the chain. The contract is simple:

```php
use Illuminate\Pipeline\Pipeline;

$result = app(Pipeline::class)
    ->send($order)
    ->through([
        ValidateOrderInventory::class,
        ApplyLoyaltyDiscount::class,
        CalculateTax::class,
        ReserveStock::class,
    ])
    ->thenReturn();

```

Each pipe receives `($passable, $next)` — identical to middleware — but the passable is your domain object, not an HTTP request.

### Defining Pipe Classes

Implement the implicit pipe contract with a `handle` method:

```php
final class ApplyLoyaltyDiscount
{
    public function handle(Order $order, Closure $next): Order
    {
        if ($order->customer->hasLoyaltyTier()) {
            $order->applyDiscount(
                $order->customer->loyaltyDiscountRate()
            );
        }

        return $next($order);
    }
}

```

The pipe returns `$next($order)` to continue or returns early to short-circuit. No base class required — just a callable shape.

### Using a Custom Method Name

If you want a more expressive interface, define your own method and tell the pipeline:

```php
interface OrderPipe
{
    public function process(Order $order, Closure $next): Order;
}

$result = app(Pipeline::class)
    ->send($order)
    ->through($pipes)
    ->via('process')
    ->thenReturn();

```

This lets you enforce the interface in your pipe classes and get IDE autocompletion on the passable type.

### Short-Circuiting with Exceptions vs. Early Returns

Two clean patterns exist for halting a pipeline:

**Exception-based** — throw a domain exception inside a pipe; catch it at the call site:

```php
public function handle(Order $order, Closure $next): Order
{
    if ($order->total()->isZero()) {
        throw new InvalidOrderException('Order total cannot be zero.');
    }
    return $next($order);
}

```

**Result-object-based** — wrap the passable in a result DTO so pipes can inspect and propagate failure without exceptions:

```php
public function handle(OrderResult $result, Closure $next): OrderResult
{
    if ($result->failed()) {
        return $result; // skip remaining pipes
    }
    // ... mutate and continue
    return $next($result);
}

```

The result-object approach is friendlier to testing and avoids exception-as-control-flow.

### Resolving Pipes from the Container

String class names are resolved via the service container, so constructor injection works automatically:

```php
final class CalculateTax
{
    public function __construct(
        private readonly TaxRateRepository $rates
    ) {}

    public function handle(Order $order, Closure $next): Order
    {
        $order->setTax($this->rates->rateFor($order->shippingRegion()));
        return $next($order);
    }
}

```

No service locator boilerplate — the container handles it.

### Testing Individual Pipes in Isolation

Because each pipe is a plain class, unit testing is trivial:

```php
it('applies loyalty discount when customer has a tier', function () {
    $customer = Customer::factory()->withLoyaltyTier('gold')->make();
    $order = Order::factory()->for($customer)->make(['subtotal' => 100_00]);

    $pipe = new ApplyLoyaltyDiscount();
    $result = $pipe->handle($order, fn ($o) => $o);

    expect($result->discount())->toBe(10_00);
});

```

The `$next` closure is just an identity function in tests. No HTTP context, no mocking the pipeline itself.

### When to Reach for a Pipeline

- Multi-step import/export processing (CSV → validate → transform → persist)
- Order fulfilment workflows with optional stages
- Notification enrichment chains (resolve recipient → render template → choose channel)
- API response decoration across multiple transformers

### Takeaways

- `Pipeline` is a general-purpose tool — not HTTP-only.
- Pipes are plain classes resolved by the container; constructor injection is free.
- Use `->via('method')` to enforce a typed interface across all pipes.
- Result objects are cleaner than exceptions for expected failure paths.
- Each pipe is independently unit-testable with a simple identity `$next`.
- Pipelines make multi-step domain logic explicit, ordered, and easy to extend.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fthe-pipeline-pattern-in-laravel-building-custom-pipelines-beyond-middleware&text=The+Pipeline+Pattern+in+Laravel%3A+Building+Custom+Pipelines+Beyond+Middleware) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fthe-pipeline-pattern-in-laravel-building-custom-pipelines-beyond-middleware) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Can I mix closure pipes and class-based pipes in the same pipeline?        Yes. Laravel's Pipeline accepts any combination of class name strings, object instances, and closures in the `through()` array. Closures receive the same `($passable, $next)` signature as class-based pipes. 

      Q02  How do I conditionally add a pipe based on runtime state?        Build the pipes array before passing it to `through()`. Use a standard `if` statement or `array_filter` to include or exclude specific pipe classes based on your domain conditions, then pass the final array to the pipeline. 

      Q03  Is there a performance cost to using Pipeline for domain logic instead of plain method calls?        The overhead is negligible for typical domain workflows — it amounts to a few closure wraps and container resolutions. Only in tight loops processing thousands of items per request would you consider inlining the logic, and even then profiling should guide that decision. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Eloquent Custom Casts: Encapsulating Value Objects Without the Bloat](https://cdn.msaied.com/174/2c75896ee4182bb2f66e2c93bed18796.png) laravel eloquent ddd 

### Eloquent Custom Casts: Encapsulating Value Objects Without the Bloat

Custom Eloquent casts let you bind rich value objects directly to model attributes. This article shows how to...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 14 Jun 2026     1 min read  

  Read    

 ](https://msaied.com/articles/eloquent-custom-casts-encapsulating-value-objects-without-the-bloat) [ ![Filament v4 Schema-Based Forms: Unified Schema API in Practice](https://cdn.msaied.com/173/6e0d9faa9137cb296e37831c3645e7ba.png) filament laravel filament-v4 

### Filament v4 Schema-Based Forms: Unified Schema API in Practice

Filament v4 replaces scattered form/infolist definitions with a single Schema API. This post walks through rea...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 14 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/filament-v4-schema-based-forms-unified-schema-api-in-practice) [ ![Read/Write Splitting, Connection Pooling, and Sticky Reads in Laravel](https://cdn.msaied.com/171/92ea6daa5c8b599f43ef1c69a7c1eaa6.png) laravel database performance 

### Read/Write Splitting, Connection Pooling, and Sticky Reads in Laravel

Learn how Laravel's database layer handles read/write splitting, when sticky reads save you from replication l...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 14 Jun 2026     4 min read  

  Read    

 ](https://msaied.com/articles/readwrite-splitting-connection-pooling-and-sticky-reads-in-laravel) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
