0% прочитано

I used Laravel Pipeline for commission calculation and the logic became much clearer

Too many business rules usually lead to messy code. Here's how Laravel Pipeline helped organize a complex commission calculation into a clean and readable flow.

21 липня 2026 р. 4 хв читанняLaravel

When a sequence of conditions became a separate process

Each individual rule was relatively small. The real problem was not the complexity of a single condition, but the number of rules and the order in which they had to run.

I wanted:

  • each rule to remain a separate class;

  • the priority order to be visible in one place;

  • a rule to either return a result or pass processing forward;

  • every step to be testable independently.

Laravel Pipeline turned out to be a good fit for this.

Domain class names and some implementation details in this example have been changed. The Pipeline structure and the interaction between its steps remain the same as in the production solution.

A shared contract for every step

Each step receives the calculation context and a callback for the next step.
<?php declare(strict_types=1); namespace App\Services\Commission\Steps; use App\DTO\CommissionContext;use Closure; interface CommissionStepInterface{    public function handle(        CommissionContext $context,        Closure $next,    ): array;}

This contract makes every rule look the same from the Pipeline’s perspective. Internally, the steps may check completely different conditions, but they interact with the process in a consistent way.

The rule order is visible in one place

php
<?php declare(strict_types=1); namespace App\Services\Commission; use App\DTO\CommissionContext;use App\Services\Commission\Steps\CampaignCommissionStep;use App\Services\Commission\Steps\CustomerTierCommissionStep;use App\Services\Commission\Steps\DefaultCommissionStep;use App\Services\Commission\Steps\PreferredPartnerCommissionStep;use App\Services\Commission\Steps\ProductCategoryCommissionStep;use App\Services\Commission\Steps\VolumeCommissionStep;use Illuminate\Pipeline\Pipeline; final readonly class CommissionPipeline{    public function __construct(        private Pipeline $pipeline,    ) {}     public function calculate(CommissionContext $context): array    {        return $this->pipeline            ->send($context)            ->through($this->steps())            ->thenReturn();    }     private function steps(): array    {        return [            PreferredPartnerCommissionStep::class,            ProductCategoryCommissionStep::class,            CustomerTierCommissionStep::class,            VolumeCommissionStep::class,            CampaignCommissionStep::class,            DefaultCommissionStep::class,        ];    }}

This is the part of the solution I like the most.

To understand the calculation priority, I no longer need to open a large method and inspect nested if statements. I can simply read the list of steps from top to bottom.

Special rules run first. If none of them applies, the final step returns the default commission.

One of the іteps

The other steps behave slightly differently.
<?php declare(strict_types=1); namespace App\Services\Commission\Steps; use App\DTO\CommissionContext;use App\Helpers\Percent;use App\Services\Commission\CommissionPolicy;use Closure; final readonly class DefaultCommissionStep implements CommissionStepInterface{    public function __construct(        private MinimumCommissionApplier $minimumCommissionApplier,        private CommissionPolicy $commissionPolicy,    ) {}     public function handle(        CommissionContext $context,        Closure $next,    ): array {        $percent = $this->commissionPolicy->defaultPercent(            $context->subject,            $context->amount,        );         return [            'commission' => $this->minimumCommissionApplier->apply(                Percent::amount($context->amount, $percent),                $context,            ),            'original_percent' => $percent,            'percent' => $percent,            'rule' => 'default',        ];    }}

What changed after this

The calculation did not become smaller in terms of business rules. But its structure became much easier to understand.

Now:

  • every rule has a separate responsibility;

  • the execution order is immediately visible;

  • a new scenario can be added as another step;

  • individual rules are easier to test;

  • the main service no longer grows with every new condition.

Separate tests are especially important for calculation logic. In another case, a test was exactly what showed me that the same calculation using round() produced a different result on PHP 8.3 and PHP 8.4, even though the code itself had not changed.

For me, this was one of those cases where a pattern did more than make the code look cleaner. It made the business process visible in the program structure.

Yakymiv Alyona

Takeaway

Laravel Pipeline is often associated with middleware or sequential data processing. In this case, it worked well for an ordered collection of commission rules.

The biggest benefit was not the removal of conditional statements. They still exist, but now each one lives in the class where it belongs.

The real benefit was that the order became explicit, every step received a clear responsibility, and the calculation stopped looking like one large method.