DEV Community

Cover image for Middleware in Business Logic?
Andrej Rypo
Andrej Rypo

Posted on • Updated on

Middleware in Business Logic?

In case you are into web development, you might be familiar with the concept of HTTP middleware, which is common in microframeworks (Slim PHP, Silex), custom stack APIs and even some of the full stack frameworks (Laravel, CakePHP) and can be added to most other frameworks too.
But have you ever thought of using middleware in your business logic? Does it make sense? Let's find out.


Imagine a fairly simple calculation of price of an order in an e-commerce solution:

function calculateOrderSum(Order $order)
{
    // calculate the sum of the item prices
    $subtotal = array_reduce(
        $order->items, 
        fn($carry, $item) => $carry + $item->price, 
        0
    );

    // calculate coupon discount
    $discount = CouponService::discountForCoupon($order->coupon);

    // calculate VAT
    $vat = $subtotal * 0.1;

    // calculate shipping cost
    $shipping = ShippingService::calculateShipping(
        $order->delivery->region
    );

    // calculate total
    return $subtotal + $discount + $vat + $shipping;
}
Enter fullscreen mode Exit fullscreen mode

This is quite simple and looks OK.
Now imagine the same calculation, but for a multi-tenant app, or even a SAAS solution.

Imagine a single code base deployed to the cloud serving multiple domains with different e-shops.

Each tenant can use certain addons or implement specific way to calculate the price:

function calculateOrderSum(Order $order, Tenant $tenant)
{
    // calculate the sum of the items
    $subtotal = array_reduce(
        $order->items,
        fn($carry, $item) => $carry + $item->price,
        0
    );

    // apply coupon discount
    $discount = 0;
    if ($tenant->usesCoupons) {
        $discount += CouponService::discountForCoupon($order->coupon);
    }
    if ($tenant->usesPriceBasedDiscounts) {
        $discount += DiscountService::calculateForSubtotal($subtotal);
    }

    // apply VAT
    $vat = VatService::calculateVat($subtotal + $discount);

    // calculate shipping cost
    $shipping = 0;
    if ($subtotal < $tenant->shippingFreeMinimum) {
        $shipping = ShippingService::calculateShipping(
            $order->delivery->region
        );
    }

    // calculate total
    return $subtotal + $discount + $vat + $shipping;
}
Enter fullscreen mode Exit fullscreen mode

Above, we only added 2 simple conditions and already the solution became less legible.
We needed to add the Tenant parameter to a calculation that should not care for tenants and such, it's not its concern ✋.

In reality, tenants will offer loayalty discounts, bundle discounts and whatnot. Tenants in different regions will calculate VAT differently and may even include some other tax than VAT. That's a lot of ifs and thens!

What is more, this tangled mess becomes very hard to test.

Decomposition

The above algorithm can be decomposed to several simple blocks:

$subtotalCalc = function (Order $order, callable $next) {
    $price = array_reduce(
        $order->items,
        fn($carry, $item) => $carry + $item->price,
        0
    );
    return $next($order) + $price;
};
$couponDiscountCalc = function (Order $order, callable $next) {
    return $next($order) + CouponService::discountForCoupon(
            $order->coupon
        );
};
$discountCalc = function (Order $order, callable $next) {
    $subtotal = $next($order);
    return $subtotal + DiscountService::calculateForSubtotal($subtotal);
};
$shippingCalc = function (Order $order, callable $next) {
    return $next($order) + ShippingService::calculateShipping(
            $order->delivery->region
        );
};
// a parameterized factory:
$vatCalcFactory = function (float $vat): callable {
    return function (Order $order, callable $next) use ($vat) {
        return $next($order) * (1 + $vat / 100);
    };
};
Enter fullscreen mode Exit fullscreen mode

Each of them is independent and can very simply be unit tested. What is more, we can create them on the fly and parameterize them as we need.
A huge benefit is that the complexity of the algorithm does not increase with added logic, since you are only adding independent blocks.

Blocks of code like these are called middleware. Observe the base structure of a middleware:

function middleware(mixed $passable, callable $next): mixed {

    // 1. code that happens before the next middleware
    // Note: this code may alter the argument

    // 2. invoke the next middleware
    // Note: this step is optional too
    $result = $next($passable);

    // 3. code that happens after the next middleware returns
    // Note: this code may alter the result

    return $result;
}
Enter fullscreen mode Exit fullscreen mode

When composed, a middleware stack may be perceived as layers of an onion added one on top of the other. Note that the last added (outer-most) layer is executed first.

Composition

Now we can elegantly build different computation pipelines for each tenant.

$bikeShop = Pipeline::onion([
    $subtotalCalc,
    $couponDiscountCalc,
    $vatCalcFactory(10.0),
    $shippingCalc,
]);
$europeanToolsShed = Pipeline::onion([
    $subtotalCalc,
    $couponDiscountCalc,
    $discountCalc,
    LoyaltyService::forCustomer($currentlyAuthenticatedUser),
    $vatCalcFactory(20.0),
    $shippingCalc,
]);
Enter fullscreen mode Exit fullscreen mode

To calculate the order's total, we only need to invoke the proper pipeline, like so:

$total = $bikeShop($order);
// or
$total = $europeanToolsShed($order);
Enter fullscreen mode Exit fullscreen mode

I'm using the Pipeline class from my own package dakujem/cumulus, but there are other middleware/pipeline dispatchers out there. Just make sure not to confuse them with HTTP middleware dispatchers, they are different animals.

Wrapping Up

Using middleware pattern and composition, it is possible to reduce complexity of a complex calculation resulting in easier to understand and easier to test codebase.
This pattern is especially useful when the actual steps of a calculation are not known beforehand (avoiding heaps of ifs!).

If you are new to middleware, be sure to google some info on HTTP middleware and PSR-15.

To build and dispatch simple pipelines within your domain logic, check out the Pipeline dispatcher.

GitHub logo dakujem / cumulus

☁️ Plug-in utilities for cloud-enabled software. Framework agnostic.

Discussion (0)