Laravel Custom Pipelines 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)    Laravel Pipeline Pattern: 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 Facade Actually Does ](#what-the-pipeline-facade-actually-does)
3. [  Typed Pipe Classes ](#typed-pipe-classes)
4. [  Carrying State with a Context DTO ](#carrying-state-with-a-context-dto)
5. [  Short-Circuiting and Early Returns ](#short-circuiting-and-early-returns)
6. [  Building a Reusable Pipeline Service ](#building-a-reusable-pipeline-service)
7. [  Testing Individual Pipes ](#testing-individual-pipes)
8. [  Takeaways ](#takeaways)

  ![Laravel Pipeline Pattern: Building Custom Pipelines Beyond Middleware](https://cdn.msaied.com/233/85e36a08351c8eb2be511c1a236b5dad.png)

  #laravel   #pipeline   #clean-architecture   #php  

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

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

       Table of contents

1. [  01   The Pipeline Pattern in Laravel: Beyond Middleware  ](#the-pipeline-pattern-in-laravel-beyond-middleware)
2. [  02   What the Pipeline Facade Actually Does  ](#what-the-pipeline-facade-actually-does)
3. [  03   Typed Pipe Classes  ](#typed-pipe-classes)
4. [  04   Carrying State with a Context DTO  ](#carrying-state-with-a-context-dto)
5. [  05   Short-Circuiting and Early Returns  ](#short-circuiting-and-early-returns)
6. [  06   Building a Reusable Pipeline Service  ](#building-a-reusable-pipeline-service)
7. [  07   Testing Individual Pipes  ](#testing-individual-pipes)
8. [  08   Takeaways  ](#takeaways)

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

Most Laravel developers know `Pipeline` as the engine behind HTTP middleware. Fewer reach for it when modelling domain workflows — and that's a missed opportunity. The `Illuminate\Pipeline\Pipeline` class is a general-purpose, composable tool that can replace tangled chains of service calls, nested conditionals, and procedural scripts.

### What the Pipeline Facade Actually Does

At its core, a pipeline passes a *payload* through an ordered list of *pipes*, each of which can transform the payload or short-circuit the chain.

```php
use Illuminate\Support\Facades\Pipeline;

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

```

Each pipe receives `($payload, Closure $next)` and must call `$next($payload)` to continue — identical to middleware, but with no HTTP coupling whatsoever.

### Typed Pipe Classes

Anonymous closures work for quick scripts, but named classes are the right choice for production pipelines. Make the contract explicit with an interface:

```php
interface OrderPipe
{
    public function handle(OrderContext $context, Closure $next): OrderContext;
}

```

Then implement each step as a focused, injectable class:

```php
final class ApplyDiscounts implements OrderPipe
{
    public function __construct(
        private readonly DiscountRepository $discounts
    ) {}

    public function handle(OrderContext $context, Closure $next): OrderContext
    {
        $applicable = $this->discounts->forCustomer($context->customer);

        return $next(
            $context->withDiscount($applicable->totalAmount())
        );
    }
}

```

Because Laravel resolves pipe classes through the service container, constructor injection works automatically — no factory boilerplate needed.

### Carrying State with a Context DTO

Passing a mutable array through a pipeline is fragile. Instead, use an immutable value object (or a simple DTO with `with*` wither methods) as the payload:

```php
final class OrderContext
{
    public function __construct(
        public readonly Order $order,
        public readonly Customer $customer,
        public readonly Money $discount = new Money(0),
        public readonly Money $tax = new Money(0),
    ) {}

    public function withDiscount(Money $discount): self
    {
        return new self($this->order, $this->customer, $discount, $this->tax);
    }

    public function withTax(Money $tax): self
    {
        return new self($this->order, $this->customer, $this->discount, $tax);
    }
}

```

Each pipe returns a new context rather than mutating shared state, making the data flow trivially traceable.

### Short-Circuiting and Early Returns

Sometimes a pipe should halt the chain — for example, when inventory is insufficient:

```php
final class ValidateInventory implements OrderPipe
{
    public function handle(OrderContext $context, Closure $next): OrderContext
    {
        foreach ($context->order->lines as $line) {
            if (! $line->product->hasStock($line->quantity)) {
                // Return without calling $next — pipeline stops here.
                return $context->withError(
                    "Insufficient stock for {$line->product->sku}"
                );
            }
        }

        return $next($context);
    }
}

```

The caller inspects `$result->hasError()` after `thenReturn()`. No exceptions required for expected business failures.

### Building a Reusable Pipeline Service

For pipelines used in multiple places, wrap them in a dedicated service:

```php
final class OrderProcessingPipeline
{
    private array $pipes = [
        ValidateInventory::class,
        ApplyDiscounts::class,
        CalculateTax::class,
        ReserveStock::class,
    ];

    public function __construct(private readonly Pipeline $pipeline) {}

    public function process(OrderContext $context): OrderContext
    {
        return $this->pipeline
            ->send($context)
            ->through($this->pipes)
            ->thenReturn();
    }
}

```

Bind it in a service provider and inject it wherever needed. Swapping or reordering pipes becomes a one-line change.

### Testing Individual Pipes

Because each pipe is a plain class, unit testing is straightforward with Pest:

```php
it('applies the highest available discount', function () {
    $discounts = Mockery::mock(DiscountRepository::class);
    $discounts->allows('forCustomer')->andReturn(
        new DiscountCollection([new Discount(Money::of(20, 'GBP'))])
    );

    $pipe = new ApplyDiscounts($discounts);
    $context = OrderContext::fake();

    $result = $pipe->handle($context, fn ($ctx) => $ctx);

    expect($result->discount->amount())->toBe(20);
});

```

No HTTP kernel, no database — just the pipe and its collaborators.

### Takeaways

- Use `Pipeline::send()->through()->thenReturn()` for any multi-step domain workflow, not just HTTP.
- Typed pipe interfaces and immutable context DTOs eliminate ambiguity about what each step receives and returns.
- Container resolution means constructor injection works in every pipe class for free.
- Short-circuit by returning the payload without calling `$next` — no exceptions needed for expected failures.
- Wrapping pipelines in a dedicated service class makes reuse, testing, and pipe reordering trivial.

 Found this useful?

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

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

  3 questions  

     Q01  Can I use the Laravel Pipeline outside of HTTP middleware?        Yes. `Illuminate\Pipeline\Pipeline` is a standalone class with no HTTP coupling. You can inject it into any service and use it for domain workflows, data transformation, import processing, or any multi-step operation. 

      Q02  How do I stop a pipeline early without throwing an exception?        Simply return the payload from a pipe without calling `$next($payload)`. The pipeline stops at that pipe and `thenReturn()` returns the current payload. Model expected failures as data on the context object rather than exceptions. 

      Q03  Are pipe classes resolved from the service container?        Yes. When you pass class name strings to `through()`, Laravel resolves each one via `app()-&gt;make()`, so constructor dependencies are injected automatically just like controllers or jobs. 

  Continue reading

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

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

 [ ![PostgreSQL CTEs, Recursive Queries, and Lateral Joins in Laravel](https://cdn.msaied.com/241/32858f9c67eae0649999c32a6d31818f.png) laravel postgresql query-builder 

### PostgreSQL CTEs, Recursive Queries, and Lateral Joins in Laravel

Go beyond basic Eloquent with raw PostgreSQL power: composable CTEs, recursive tree traversal, and LATERAL joi...

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

 19 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/postgresql-ctes-recursive-queries-and-lateral-joins-in-laravel) [ ![PostgreSQL Window Functions in Laravel: Ranking, Running Totals, and Gap Detection](https://cdn.msaied.com/239/f588e7cbf8e6d3317a581ce0fa27140d.png) laravel postgresql eloquent 

### PostgreSQL Window Functions in Laravel: Ranking, Running Totals, and Gap Detection

Window functions let you compute rankings, running totals, and gaps directly in SQL without pulling rows into...

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

 19 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/postgresql-window-functions-in-laravel-ranking-running-totals-and-gap-detection) [ ![Custom Eloquent Casts: Encapsulating Domain Logic Inside Model Attributes](https://cdn.msaied.com/238/8e843e57a34f81f853eedefae629c09b.png) laravel eloquent domain-driven-design 

### Custom Eloquent Casts: Encapsulating Domain Logic Inside Model Attributes

Custom Eloquent casts let you push value-object logic directly into model attributes, keeping controllers and...

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

 19 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/custom-eloquent-casts-encapsulating-domain-logic-inside-model-attributes) 

   [  ![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)
