Design patterns for caching in Laravel

When talking about making performance improvements to an application, the first thing that comes to mind is caching.

The next thought that usually follows is, ‘What is the easiest and quickest way to implement it?’.

When building software, inevitably there comes the stage where the development team will start talking about performance improvements. And that’s when fingers get pointed.

Typically it’s one of the last things a team think about due to time constraints and the tangibility of it. Product Owners will tend to push for the tangible features until the entire team realises there’s a problem with performance.

That’s why we, as developers, should think about the application’s performance at the very start of the project. Yes, even before we open the code editor and write the very first line. It’s something that we should be raising from the bottom up as part of features that are required to be part of the core and discuss with the team along with Product Owners to ensure it goes into early implementation.

I’ll take an example from the project I’m currently working on.

In my team, we were fortunate that performance improvements could be made without making changes to the core code base. The application is built using nested set model (Tree structure). We use MongoDB to store the data and the application lifts the weight of object loading.

In simple terms, we have only one class which is responsible for all data objects in the system. Yes, there are a few more classes that help handling data validation, related data loading, and more, but the core of the application is one class. Any data point in the system is an instance of that main class.

The Problem

This architecture has helped us scale the system without having to make any core changes to the code base. In fact, besides maintenance, the core of the application hasn’t seen any added features in years.

But even though this architecture has lots of pros in our case, it has its drawbacks when it comes to performance.

Our benchmark was for any endpoint to load within less than a second. Our application is very data heavy, and some endpoints were taking up to 4 seconds when doing calculations against big datasets.

As we looked into the issue, it turned out to be that some helper classes responsible for calculations were regularly being called. Most noticable 3 or 4 levels down the chain due to the tree architecture.

We made two key changes to improve performance.

  1. The first solution was to optimise the methods that load data objects. We used lazy loading on all the nested data relationships to reduce this stress.
  2. The second solution was to cache data that was already being called during the process.

Architecture of the application

Before we dive deeper into cache implementation, let’s have a look at the application structure.

  • Backend API - Laravel
  • Frontend - VueJS
  • Database - mongoDB
  • Cache - Redis
  • The application is also deployed using Docker and deployment is done in separate containers.

Looking at the architecture of the application, we had plenty of freedom to optimise along many levels.

Cache Implementation

Before we got started, we made few rules:

  1. We could only change the core code as a last resort.
  2. We needed to utilise our existing resources
  3. Implementation shouldn’t take long, to avoid any major refactoring.

Given the above constraints, it was clear we had to use an observer pattern as a wrapper to the application so we wouldn’t touch the core code.

Implementation

Now we knew we were building the cache layer using an observer pattern, we started utilising Laravel’s Service Container.

And since the bottleneck was the constant data load from the database, we started by injecting a concrete implimentation of the data class.

Inject concrete class with the help of the service provider

class BusinessPlansServiceProvider extends ServiceProvider
{

    public function register()
    {
        $this->app->singleton(Plan::class, function () {
            // return new Plan object;
            return new Plan();
        });

        $this->app->singleton(PlanDataList::class, function () {
            // return new Table;
            return new PlanDataList();
        });
    }
}

Resolving injected class within the logic class

    protected function addBucket($name, $key)
    {
        $data = app(PlanDataList::class);
        $data->setPlanId($this->getPlanId());
        $loadedData = $data->loadData($name);

        $this->addRow(is_string($key) ? $key : $name, $loadedData);
    }

The next step was to create interfaces with data fetching methods. The reason behind this approach was to have a class that can load data from the cache with the same method names. This way, there was no need to change the core code.

Interfaces

Once we had all those heavy data loading methods being extracted to interfaces, it was time to implement cache supported class.

PlanDataListInterface.php

interface PlanDataListInterface
{
    public function setPlanId(string $planId);
    public function loadData(string $bucket);
}

PlanInterface.php

interface PlanInterface
{
    public function getPlan(string $planId);
}

Protip: Be wise when creating your cache tags and remember to use short cache tag names. Redis will thank you in the long run.

Cache supported class

Once we created the cache supported classes, we could easily replace them with our concrete class with the help of the Service Container.

class BusinessPlansServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(Plan::class, function () {
            // return new Plan object;
            return new PlanCache(new Plan());
        });

        $this->app->singleton(PlanDataList::class, function () {
            // return new Table;
            return new PlanDataCache(new PlanDataList());
        });
    }
}

PlanCache.php

class PlanCache implements PlanInterface
{
    use CacheTracker;
    const CACHE_LIFE_TIME = 8;

    private $next;

    public function __construct(Plan $next)
    {
        $this->next = $next;
    }

    public function getPlan(string $planId)
    {
        $expiresAt = Carbon::now()->addHours(self::CACHE_LIFE_TIME);

        return Cache::tags(CacheTracker::genCacheTags())
                ->remember(CacheTracker::genCacheKey(['get_plan',]), $expiresAt,
                    function () use ($planId) {
                        return $this->next->getPlan($planId);
                    });
    }
}

PlanDataCache.php

class PlanDataCache implements PlanDataListInterface
{
    use CacheTracker;
    const CACHE_LIFE_TIME = 8;

    private $next;

    public function __construct(PlanDataList $next)
    {
        $this->next = $next;
    }

    public function setPlanId(string $planId)
    {
        $expiresAt = Carbon::now()->addHours(self::CACHE_LIFE_TIME);

        return Cache::tags(CacheTracker::genCacheTags())
                ->remember(CacheTracker::genCacheKey(['setPlanId']), $expiresAt,
                    function () use ($planId) {
                        return $this->next->setPlanId($planId);
                    });
    }

    public function loadData(string $bucket)
    {
        $expiresAt = Carbon::now()->addHours(self::CACHE_LIFE_TIME);

        return Cache::tags(CacheTracker::genCacheTags())
                ->remember(CacheTracker::genCacheKey(['loadData', $bucket]), $expiresAt,
                    function () use ($bucket) {
                        return $this->next->loadData($bucket);
                    });
    }
}

And there you have it.

We have implemented a caching wrapper without disrupting the core of the application.

Now when the application calls any data loading methods it’ll look in the cache first before it talks to the database.

A final word: Cache expiration

As I mentioned earlier, in our application there is only one core class responsible for talking to database. We implemented cache expiration using the boot method in Elequent. But if you are not using Laravel, you can use the database model events that come with the library that you use.

CacheTracker.php was used as a helper class to support the cache implimentation

CacheTracker.php

trait CacheTracker
{
    public static function bootCacheTracker()
    {
        static::created(function ($instance) {
            self::flushCache();
        });

        static::deleted(function ($instance) {
            self::flushCache();
        });

        static::saved(function ($instance) {
            self::flushCache();
        });


        static::updated(function ($instance) {
            self::flushCache();
        });
    }

    public static function flushCache(array $tags = [])
    {
        if (count($tags) === 0) {
            $tags = self::genCacheTags();
        }

        Cache::tags($tags)->flush();
    }

    public static function genCacheTags()
    {
        $arrForKey = [
            'module',
            'business'
        ];

        return $arrForKey;
    }

    public static function genCacheKey($args = [])
    {
        $filterQuery = (app(FilterQuery::class));
        $filterArray = $filterQuery->getFilterArray(request());

        $businessId = $filterQuery->pluckBusinessIdFromFilter(collect($filterArray))->first();
        $planId = $filterQuery->pluck(collect($filterArray), 'id')->first();

        $arrForKey = [
            'bi_',
            $businessId,
            $planId
        ];

        $cacheKey = implode('_', array_merge($arrForKey, $args));

        return md5($cacheKey);
    }
}

Protip: When it comes to cache invalidation you need to make sure you use correct cache keys. Otherwise, it’ll drop the whole cache when you transact with the database each and every time.


Dependency Injection with PHP

Dependency Injection. Everyone has heard about it, but do they really know it?

This is a common theme I get from most candidates I have interviewed in the past couple of years when looking for Software Engineers to work on our SaaS based BI platform EngineRoom at Digital360. But when I dig a little deeper into solutions they have built in the past, it has shown that most haven’t followed proper implementation techniques.

In this blog post, I’ll try to explain my take on dependency injection, and in particular how to implement a scalable solution.

What is a dependency?

According to Wikipedia. In software engineering, dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object. A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it.

In this context an object (A) that expects another object’s (B) help to complete a job that object (A) is responsible for. To explain the theory, I’ll build a simple music player that can be used against multiple music services like Google Music, Spotify, etc.

Objective

Music player should be able to play music from user’s preferred music service.

ProTip: How to find which dependencies to inject?

If we look at the above requirement, the music player is more on the parental side, it requires any number of music services to function. Therefore it makes sense to inject the music service into the music player.

Approaches

Common Approach

This is a common approach that doesn’t use Dependency Injection

class MusicPlayer
{
    private $musicService;

    public function __construct(GoogleMusic $googleMusic)
    {
        $this->musicService = $googleMusic;
    }
}

If you look at the code above, someone might say:

“yeah that’s dependency injection.”

But, due to that method injecting the object into the MusicPlayer class using the constructor, this is considered Constructor Injection.

This is considered bad practice as it permanently binds the GoogleMusic class to the MusicPlayer class.

Another example is creating the GoogleMusic object inside the MusicPlayer class.

class MusicPlayer
{
    private $musicService;

    public function __construct()
    {
        $this->musicService = new GoogleMusic();
    }
}

Both code snippets above are doing the same mistake. They are permanently binding the GoogleMusic service to the MusicPlayer class. Now the MusicPlayer class has become unusable for anything other than the GoogleMusic service, so other services like the SpotifyMusic service cannot be used.

Let’s add some support for the SpotifyMusic service.

class MusicPlayer
{
    private $musicService;

    public function __construct(GoogleMusic $googleMusic, SpotifyMusic $spotifyMusic)
    {
    
        $this->musicService = null;
        
        if($user->getMusicService()->getName() == 'GOOGLE_MUSIC') {
            $this->musicService = $googleMusic;
        }
        
        if($user->getMusicService()->getName() == 'SPOTIFY_MUSIC') {
            $this->musicService = $spotifyMusic;
        }
    }
}

In this scenario, the music player can work with Google Music and Spotify. But the music services are still coupled tightly with the player.

What issues are we are going to face if we couple our code tightly to the Music player?
  1. It’s not scalable, we can’t easily add another music service such as AppleMusic. With the current implementation, there are two ways to support this by editing both the MusicPlayer class and the AppleMusic class:

    • Inject AppleMusic as an argument
    • Initiate AppleMusic object within the constructor

A better approach

Using Dependency Injection.

First. Let’s look at the problem we are trying to solve once more.

We are building a system that can play music based on a user’s prefered service.

If you read the problem again you can see that we are going to have multiple music services. To use multiple services with dependency injections we need to build services against contracts. A contract in OOP is an Interface.

ProTip: When to create an Interface?

I have seen some developers go overboard with writing code against interfaces for everything and make the code so dynamic without having the real need. If you are going to have more than one similar type of class (Google Music, Spotify, Apple Music, etc.), then create one interface.

As we know that we are going to support more than one music service, we will be creating an IMusicService interface and an ITrack interface.

We don’t need an interface for the music player because there is only going to be one player.

But why an Interface for the Track?

We don’t know how other services will return track listings or other information, therefore to be on the safe side, we can have a separate ITrack implementation to handle separate responses.

// MusicService Interface
namespace Sample\Contracts;

interface IMusicService
{
    public function getName(): string;

    public function fetchPlayList();

    public function fetchTrack();
}
// Music Track Interface
namespace Sample\Contracts;

interface ITrack
{
    public function getName(): string;

    public function getArtist(): string;

    public function getTrackPath(): string;

    public function play();
}

Now let’s write the music player class.

namespace Sample;

use Exception;
use Sample\Contracts\IMusicService;
use Sample\Contracts\ITrack;
use Sample\Services\NoMusicService;

class MusicPlayer
{
    private $musicService;

    /**
     * MusicPlayer constructor.
     *
     * @param \Sample\Contracts\IMusicService $musicService
     *
     * @throws \Exception
     */
    public function __construct(IMusicService $musicService)
    {
        if ($musicService instanceof NoMusicService) {
            throw new Exception('No Music service been configured for this user');
        }

        $this->musicService = $musicService;
    }

    public function play()
    {
        /** @var ITrack $track */
        $track = $this->musicService->fetchTrack();
        $track->play();
    }
}

If you look at the MusicPlayer constructor, it takes one argument. It’s the Music service Interface.

It’s time to let the music player play some tracks from the injected services.

require __DIR__ . '/vendor/autoload.php';

use Sample\Contracts\IMusicService;
use Sample\MusicPlayer;
use Sample\Services\Google\GoogleMusic;
use Sample\Services\NoMusicService;
use Sample\Services\Spotify\SpotifyMusic;

switch (strtolower($_GET['user'])) {
    case 'alice':
        $musicService = new GoogleMusic();
    case 'bob':
        $musicService = new SpotifyMusic();
    default:
        $musicService = new NoMusicService();
}

try {
    $musicPlayer = new MusicPlayer($container->make($musicService));
    $musicPlayer->play();
} catch (Exception $e) {
    die('Can\t find a music service.');
}

The ideal approach.

We can use an IoC container (Inversion of Control) to handle the creation of these objects and not worry about it again until there is a new service that needs to be supported.

As IoC is its own topic I won’t get too deep on that subject here. For the moment think about it as a dynamic object injection based on required dependencies by the MusicPlayer class (although it’s much more than that).

In the example that follows, I’ll be using Laravel’s IoC container.

Once the IoC installation is done using composer, we can create an IoC instance and let it handle injection to the MusicPlayer instance based on the logic we want.

In our case loading different music service objects based on different user preferences.

require __DIR__ . '/vendor/autoload.php';

use Illuminate\Container\Container;
use Sample\Contracts\IMusicService;
use Sample\MusicPlayer;
use Sample\Services\Google\GoogleMusic;
use Sample\Services\NoMusicService;
use Sample\Services\Spotify\SpotifyMusic;

// Create new IoC Container instance
$container = Container::getInstance();

$container->singleton(IMusicService::class, function ($app) {
    switch (strtolower($_GET['user'])) {
        case 'alice':
            return new GoogleMusic();
        case 'bob':
            return new SpotifyMusic();
        default:
            return new NoMusicService();
    }
});

try {
    $musicPlayer = new MusicPlayer($container->make(IMusicService::class));
    $musicPlayer->play();
} catch (Exception $e) {
    die('Can\t find a music service.');
}

Conclusion

Test the code!

You can find the completed code in the Github. It comes with a Dockerfile for you to try it out.

If you have any questions/suggestions around the implementation feel free to contact me.