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)    Laravel Pipeline Pattern: Building Custom Pipelines Beyond Middleware        On this page       1. [  The Pipeline Pattern Beyond Middleware ](#the-pipeline-pattern-beyond-middleware)
2. [  Why Reach for a Pipeline? ](#why-reach-for-a-pipeline)
3. [  Defining a Typed Payload ](#defining-a-typed-payload)
4. [  Writing Stages ](#writing-stages)
5. [  Assembling the Pipeline ](#assembling-the-pipeline)
6. [  Short-Circuiting a Stage ](#short-circuiting-a-stage)
7. [  Testing Stages in Isolation with Pest ](#testing-stages-in-isolation-with-pest)
8. [  Registering Stages via the Container ](#registering-stages-via-the-container)
9. [  Takeaways ](#takeaways)

  ![Laravel Pipeline Pattern: Building Custom Pipelines Beyond Middleware](https://cdn.msaied.com/499/6113dcde1518951ad514082a3699232a.png)

  #laravel   #design-patterns   #architecture   #testing  

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

     2 Aug 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said  

       Table of contents

  9 sections  

1. [  01   The Pipeline Pattern Beyond Middleware  ](#the-pipeline-pattern-beyond-middleware)
2. [  02   Why Reach for a Pipeline?  ](#why-reach-for-a-pipeline)
3. [  03   Defining a Typed Payload  ](#defining-a-typed-payload)
4. [  04   Writing Stages  ](#writing-stages)
5. [  05   Assembling the Pipeline  ](#assembling-the-pipeline)
6. [  06   Short-Circuiting a Stage  ](#short-circuiting-a-stage)
7. [  07   Testing Stages in Isolation with Pest  ](#testing-stages-in-isolation-with-pest)
8. [  08   Registering Stages via the Container  ](#registering-stages-via-the-container)
9. [  09   Takeaways  ](#takeaways)

       The Pipeline Pattern Beyond Middleware
--------------------------------------

Most Laravel developers know `Pipeline` from HTTP middleware, but the same `Illuminate\Pipeline\Pipeline` class is available anywhere in your application. Used deliberately, it replaces tangled service methods with a clean, composable chain of single-responsibility stages.

### Why Reach for a Pipeline?

Consider an order-submission workflow: validate inventory, apply discount rules, charge payment, dispatch fulfilment, send confirmation. Stuffing all of that into an `OrderService::submit()` method creates a god-object. A pipeline turns each concern into its own class, testable in isolation.

### Defining a Typed Payload

Start with a readonly DTO so every stage shares a common contract:

```php
readonly class OrderContext
{
    public function __construct(
        public Order $order,
        public ?Discount $discount = null,
        public bool $paymentCaptured = false,
    ) {}

    public function withDiscount(Discount $discount): self
    {
        return new self($this->order, $discount, $this->paymentCaptured);
    }

    public function markPaymentCaptured(): self
    {
        return new self($this->order, $this->discount, true);
    }
}

```

Immutable DTOs prevent stages from silently mutating shared state — a common bug in mutable pipeline payloads.

### Writing Stages

Each stage receives the payload and a `$next` closure:

```php
final class ApplyDiscountStage
{
    public function __construct(
        private readonly DiscountResolver $resolver,
    ) {}

    public function handle(OrderContext $context, Closure $next): OrderContext
    {
        $discount = $this->resolver->forOrder($context->order);

        return $next($context->withDiscount($discount));
    }
}

```

```php
final class CapturePaymentStage
{
    public function __construct(
        private readonly PaymentGateway $gateway,
    ) {}

    public function handle(OrderContext $context, Closure $next): OrderContext
    {
        $this->gateway->capture($context->order, $context->discount);

        return $next($context->markPaymentCaptured());
    }
}

```

Note the explicit return type on `handle`. Laravel's pipeline calls `handle` by convention, but you can customise the method name via `->via('process')` if you prefer.

### Assembling the Pipeline

```php
final class SubmitOrderAction
{
    public function __construct(
        private readonly Pipeline $pipeline,
    ) {}

    public function execute(Order $order): OrderContext
    {
        return $this->pipeline
            ->send(new OrderContext($order))
            ->through([
                ValidateInventoryStage::class,
                ApplyDiscountStage::class,
                CapturePaymentStage::class,
                DispatchFulfilmentStage::class,
                SendConfirmationStage::class,
            ])
            ->thenReturn();
    }
}

```

`thenReturn()` returns the final payload. Use `->then(fn ($ctx) => ...)` when you need a different return value at the end.

### Short-Circuiting a Stage

Sometimes a stage should halt the chain — for example, if inventory is unavailable:

```php
final class ValidateInventoryStage
{
    public function handle(OrderContext $context, Closure $next): OrderContext
    {
        if (! $context->order->hasStock()) {
            throw new InsufficientStockException($context->order);
        }

        return $next($context);
    }
}

```

Throw a domain exception rather than returning early without calling `$next`. This keeps the pipeline contract honest and lets callers handle failures uniformly.

### Testing Stages in Isolation with Pest

```php
it('applies a discount when one is available', function () {
    $resolver = Mockery::mock(DiscountResolver::class);
    $resolver->expects('forOrder')->andReturn(new Discount(10));

    $stage = new ApplyDiscountStage($resolver);
    $order = Order::factory()->make();
    $context = new OrderContext($order);

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

    expect($result->discount)->toBeInstanceOf(Discount::class)
        ->and($result->discount->percentage)->toBe(10);
});

```

Because each stage is a plain class with constructor injection, you never need to boot the full application to test it.

### Registering Stages via the Container

Laravel resolves stage class names through the service container, so constructor dependencies are injected automatically. If a stage needs contextual binding, register it in a service provider:

```php
$this->app->when(CapturePaymentStage::class)
    ->needs(PaymentGateway::class)
    ->give(StripeGateway::class);

```

### Takeaways

- Use readonly DTOs as pipeline payloads to prevent silent mutation between stages.
- Each stage is a single-responsibility class — test it without booting the framework.
- Throw domain exceptions to short-circuit; never silently skip `$next`.
- Constructor injection works automatically because Laravel resolves stages via the container.
- `->via('process')` lets you rename the handler method for semantic clarity.
- Pipelines compose naturally with actions, making complex workflows readable and auditable.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Flaravel-pipeline-pattern-building-custom-pipelines-beyond-middleware-2&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-2) 

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

  3 questions  

     Q01  Can I reuse the same Pipeline instance across multiple requests in Octane?        No. Bind `Pipeline` as a transient (non-singleton) or resolve it fresh each time via `app(Pipeline::class)`. A shared instance retains the previous payload and stages between requests, causing subtle bugs under Octane's persistent worker model. 

      Q02  What is the difference between `thenReturn()` and `then()`?        `thenReturn()` is syntactic sugar for `-&gt;then(fn ($payload) =&gt; $payload)` — it simply returns the final payload unchanged. Use `-&gt;then(Closure $destination)` when you need to transform or persist the result after all stages have run. 

      Q03  Should pipeline stages be final classes?        Marking them `final` is a good default. Stages represent a single, concrete behaviour; allowing inheritance invites accidental overrides that break the pipeline contract. If you need variation, compose a new stage rather than extending an existing one. 

  Continue reading

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

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

 [ ![Inertia DevTools Now Available for Firefox](https://cdn.msaied.com/674/445325ab535802b1b68d3adc3ada5cd0.png) Inertia.js DevTools Firefox 

### Inertia DevTools Now Available for Firefox

Inertia DevTools has landed on Firefox with full feature parity to the Chrome extension. Firefox users can now...

  ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said 

 17 Sep 2026     3 min read  

  Read    

 ](https://msaied.com/articles/inertia-devtools-now-available-for-firefox) [ ![Laravel Scalpel: Filesystem Intrusion Evidence Scanner for Laravel Apps](https://cdn.msaied.com/675/406b0f123858892b97052502c0020eac.png) security laravel php 

### Laravel Scalpel: Filesystem Intrusion Evidence Scanner for Laravel Apps

Laravel Scalpel is a post-compromise scanner that checks your deployed application's filesystem for rogue PHP...

  ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said 

 17 Sep 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-scalpel-filesystem-intrusion-evidence-scanner-for-laravel-apps) [ ![Mercure Broadcasting in Laravel 13.32](https://cdn.msaied.com/672/f3bf9ae116410789061208a12ebf2682.png) Laravel Broadcasting Mercure 

### Mercure Broadcasting in Laravel 13.32

Laravel 13.32 ships a native Mercure broadcast driver using SSE, new copyToDisk() and moveToDisk() filesystem...

  ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said 

 16 Sep 2026     3 min read  

  Read    

 ](https://msaied.com/articles/mercure-broadcasting-in-laravel-1332) 

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