When building your Laravel applications, you may sometimes need to use a NoSQL database to store and retrieve data. One popular choice is Amazon DynamoDB, a fully managed, serverless, and highly scalable NoSQL database service provided by Amazon Web Services (AWS).

In this article, we'll take a brief look at DynamoDB. We'll then delve into how to use DynamoDB as a cache store in Laravel, and how to store Laravel models in DynamoDB using the baopham/laravel-dynamodb package.

By the end of this article, you should feel confident using DynamoDB within your Laravel applications.

What is DynamoDB?

DynamoDB is a NoSQL database service provided by Amazon Web Services (AWS). It's powerful and flexible due to its fully managed, serverless, and highly scalable design.

Because DynamoDB is fully managed, you don't need to worry about maintaining the underlying infrastructure in the same way you might need to with something like a self-hosted database. Instead, you can focus on building your application rather than managing the database.

With its serverless design, you can scale DynamoDB to meet your application's demands. A correctly configured DynamoDB table can handle large amounts of requests as your application grows.

To learn more about DynamoDB, you may want to check out the official AWS DynamoDB documentation.

Alternatively, there's a great video series on YouTube that breaks down the concepts and theories used in DynamoDB: AWS DynamoDB Guides - Everything you need to know about DynamoDB.

Using DynamoDB for caching in Laravel

Now that we have a brief understanding of DynamoDB let's examine how to use it to cache data in Laravel.

To start, you'll need to create access keys in the AWS dashboard so that Laravel can access DynamoDB. If you aren't sure how to do this, you may want to refer to the official "Identity and Access Management for Amazon DynamoDB" documentation. You'll need to keep these keys secure, as they provide API access to your AWS account, and we'll store them in our Laravel application's .env file.

You'll also want to create a new DynamoDB table called "cache" with a primary string key called "key". You may want to do this manually via the AWS dashboard or programmatically using the AWS CLI or AWS SDK.

After creating your access keys, you'll need to add them to your Laravel application's .env file:

CACHE_STORE=dynamodb

AWS_ACCESS_KEY_ID=KEY-GOES-HERE
AWS_SECRET_ACCESS_KEY=ACCESS-KEY-GOES-HERE
AWS_DEFAULT_REGION=us-east-1

In the example, we're also setting the CACHE_STORE environment variable to dynamodb to tell Laravel to use DynamoDB as the cache store.

It's important to remember that you must also set the AWS_DEFAULT_REGION environment variable to the same region as your DynamoDB table. In this particular instance, we're using the us-east-1 region.

Next, for our Laravel application to communicate with DynamoDB, we must install the aws/aws-sdk-php package. You can do this via Composer by running the following command:

composer require aws/aws-sdk-php

Your Laravel application should now be configured and ready to use DynamoDB as a cache store.

Similar to how we've discussed caching in Laravel in previous articles, you can now use the Cache facade to store and retrieve items from the cache.

For example, you can read an item from DynamoDB like so:

$value = Cache::get('key');

And you can store an item in DynamoDB like so:

Cache::put('key', 'value', $seconds);

You can also use the remember method to retrieve an item from the cache or store it if it doesn't exist:

$value = Cache::remember('key', $seconds, function () {
    return 'the-value-to-be-returned';
});

In the example above, we'll first attempt to find an item in DynamoDB with a key of key. If the item exists, we'll return the cached item. Otherwise, we'll execute the closure and store the returned value in DynamoDB with a key of key.

You can also delete items from the cache using the forget method:

Cache::forget('key');

However, it's important to remember that you can't flush an entire table in DynamoDB. This means that code such as Cache::flush() and the command php artisan cache:clear won't work as expected. Attempting to run either of these will result in a RuntimeException being thrown with the error message:

DynamoDb does not support flushing an entire table. Please create a new table.

Storing Laravel models in DynamoDB

There may be times when you want to use DynamoDB to store your Laravel models. Unfortunately, Laravel doesn't support this out of the box like it does with caching. However, we can use the popular baopham/laravel-dynamodb package to achieve this.

At the time of writing, this package has over 3.25 million downloads, 470+ stars on GitHub, and 120 forks. So it's safe to assume that it is well-maintained and popular.

For the rest of this article, we'll cover the package's features that you're most likely to use in your applications. To see all the features the package provides, you may want to check out the documentation on GitHub: https://github.com/baopham/laravel-dynamodb.

Installing the package

To get started, we'll first need to install the package via Composer by running the following command:

composer require baopham/dynamodb

You'll then need to publish the package's configuration file by running the following command:

php artisan vendor:publish --provider 'BaoPham\DynamoDb\DynamoDbServiceProvider'

After this, you'll need to add your DynamoDB access keys to your Laravel application's .env file:

DYNAMODB_KEY=DYNAMO-DB-KEY-HERE
DYNAMODB_SECRET=DYNAMO-DB-SECRET-HERE
DYNAMODB_REGION=us-east-1

It's worth noting that the package doesn't automatically use the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables that we set up earlier. Instead, it uses its own environment variables. But if you'd like to, you can update your published config/dynamodb.php file to use the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables instead.

The package should now be set up and ready to use.

Storing models in DynamoDB

To store a model in DynamoDB, you'll first need to create a table in DynamoDB to store them. You can do this manually via the AWS dashboard or programmatically with the AWS CLI or AWS SDK. For example, if you want to store a Post model in DynamoDB, you'll need to create a table called posts.

Because DynamoDB is designed to be highly scalable and performant, it doesn't support auto-incrementing primary keys like traditional databases. For this reason, you can't rely on an id column being automatically incremented like you would if using a database such as MySQL. For this reason, you may opt to use a UUID as the primary key for your models.

Let's look at a basic example of how we might want to store a Post model in DynamoDB containing information about a blog post. We'll imagine the model has the following fields:

  • uuid - The unique identifier for the blog post. This will be the "partition key" of the posts table in DynamoDB.
  • title - The title of the blog post.
  • slug - The slug of the blog post.
  • content - The content of the blog post.
  • created_at - The date and time the blog post was created.
  • updated_at - The date and time the blog post was last updated.

We'll create our model using the following command:

php artisan make:model Post

This will create a model that looks like so:

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasFactory;
}

Since we're going to be using UUIDs as the primary key for our models, we'll update the model to use Laravel's Illuminate\Database\Eloquent\Concerns\HasUuids trait and define the primary key field as uuid:

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasFactory;
    use HasUuids;

    protected $primaryKey = 'uuid';
}

By defining the primary key as uuid, the package will automatically detect that this is our partition key. When we make a query to find a post by its UUID, a "query" operation will be performed. If we don't define the primary key, a "scan" operation will be performed instead, which is less efficient and can be expensive depending on the billing model you've chosen in AWS.

We'll then need to update the Post model to extend the BaoPham\DynamoDb\DynamoDbModel class instead of Laravel's Illuminate\Database\Eloquent\Model class:

namespace App\Models;

use BaoPham\DynamoDb\DynamoDbModel;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Post extends DynamoDbModel
{
    use HasFactory;
    use HasUuids;

    protected $primaryKey = 'uuid';
}

Assuming you already have a DynamoDB table called posts set up, you can now read and write Post models to and from DynamoDB using Eloquent-like syntax.

For example, to create a new Post model, you can do the following:

$post = new Post();

$post->uuid = Str::uuid()->toString();
$post->title = 'Hello World';
$post->slug = Str::slug($post->title);
$post->content = 'This is a test post';

$post->save();

Calling $post->save() this will create a new item in the posts table in DynamoDB with the attributes we've set.

You can also query for a Post model by its UUID like so:

$post = Post::find('the-uuid-of-the-post');

This will return the Post model with the UUID of the-uuid-of-the-post.

Using indexes to query models

There will likely be times when you want to query your models based on fields other than the primary/partition key. For example, you may want to find a Post model by its slug field:

$post = Post::query()
    ->where('slug', 'the-slug-goes-here')
    ->first();

If you were to run this, DynamoDB would perform a "scan" operation with a "FilterExpression" to find the item you're looking for. This essentially involves DynamoDB reading every row in the table and finding the rows with a slug attribute equal to the-slug-goes-here. As you might imagine, this can be inefficient, especially if the table contains many rows. Depending on your billing model, this might also be more expensive as you're reading more data than you need to.

To make this more efficient, we can use a Global Secondary Index (GSI) on the slug attribute. This will allow us to perform a "query" operation instead of a "scan" operation, which is much more efficient.

Before we can use the GSI, you'll need to create it in DynamoDB on the posts table. You can do this manually via the AWS dashboard or programmatically using the AWS CLI or AWS SDK.

We can then update our Post model to state that we have an index on the slug attribute using a dynamoDbIndexKeys property:

namespace App\Models;

use BaoPham\DynamoDb\DynamoDbModel;
use Illuminate\Database\Eloquent\Concerns\HasUuids;

class Post extends DynamoDbModel
{
    use HasUuids;

    protected $primaryKey = 'uuid';

    protected $dynamoDbIndexKeys = [
        'slug_index' => [
            'hash' => 'slug'
        ],
    ];
}

In this example, we've specified that we have an index on the posts table called slug_index with a hash key of slug. We can now search for items in the posts table based on the slug attribute using the GSI.

To check that the GSI is being used, the bao-pham/laravel-dynamodb package provides a toDynamoDbQuery method that you can use to dump the query to DynamoDB without sending it:

$query = Post::query()
    ->where('slug', 'hello-world')
    ->toDynamoDbQuery();

The toDynamoDbQuery method returns an instance of BaoPham\DynamoDb\RawDynamoDbQuery, containing the operation and query sent to DynamoDB. If we were to run dd($query) after the above code, we'd see the raw contents of the returned object:

BaoPham\DynamoDb\RawDynamoDbQuery {
  +op: "Scan"
  +query: array:4 [
    "TableName" => "posts"
    "FilterExpression" => "#slug = :a1"
    "ExpressionAttributeNames" => array:1 [
      "#slug" => "slug"
    ]
    "ExpressionAttributeValues" => array:1 [
      ":a1" => array:1 [
        "S" => "hello-world"
      ]
    ]
  ]
}

In the example above, we can see from the op property that DynamoDB will perform a "Scan" operation. We can also see in the query property that we'll search the posts table for items where the slug attribute equals hello-world.

If we were to update the Post model to include the GSI, we'd see something like the following:

BaoPham\DynamoDb\RawDynamoDbQuery {
  +op: "Query"
  +query: array:5 [
    "TableName" => "posts"
    "KeyConditionExpression" => "#slug = :a1"
    "IndexName" => "slug_index"
    "ExpressionAttributeNames" => array:1 [
      "#slug" => "slug"
    ]
    "ExpressionAttributeValues" => array:1 [
      ":a1" => array:1 [
        "S" => "hello-world"
      ]
    ]
  ]
}

This example shows that a "Query" operation would be performed instead of a "Scan" operation.

As you can imagine, the toDynamoDbQuery method is a valuable tool for debugging and can help ensure that you use the GSI correctly when writing your queries.

Syncing models in DynamoDB and a traditional database

So far, we've examined how to use DynamoDB to replace a traditional database. However, there may be times when you want to use both a traditional database and DynamoDB to store a particular model's data.

For example, imagine you're building an application that provides a real-time analytics dashboard for admins. You may want to store the data in a MySQL database so that the main part of your application can read and write from it. You might then store a duplicate of the data in DynamoDB so that the analytics dashboard can read from it quickly and efficiently. This hybrid approach allows you to keep the main part of your application performant while providing a fast and efficient way to read the data for the analytics dashboard.

Let's take a look at an example of how we might convert our Post model to use both a traditional database and DynamoDB. We'll first need to switch the model back to using Laravel's Illuminate\Database\Eloquent\Model class:

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasUuids;

    protected $primaryKey = 'uuid';
}

You may have noticed that we've also removed the $dynamoDbIndexKeys property from the model. This is because we're no longer using DynamoDB as the primary storage for the model, so it's unnecessary.

We'll then need to update the model to use the package's BaoPham\DynamoDb\ModelTrait trait like so:

namespace App\Models;

use BaoPham\DynamoDb\ModelTrait;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasUuids;
    use ModelTrait;

    protected $primaryKey = 'uuid';
}

By adding the trait to the model, whenever a model is created, updated, or deleted in the traditional database, the same operation will be performed in DynamoDB. This means that the model data in both databases should always be in sync.

Conclusion

In this article, we briefly examined what DynamoDB is. We then discussed how to cache your Laravel application's data using DynamoDB and used the baopham/laravel-dynamodb package to store Laravel models in DynamoDB.

I hope you'll feel confident leveraging DynamoDB in your Laravel applications the next time you need a performant and highly scalable database!

Get the Honeybadger newsletter

Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo
    Ashley Allen

    Ashley is a freelance Laravel web developer who loves contributing to open-source projects and building exciting systems to help businesses succeed.

    More articles by Ashley Allen
    An advertisement for Honeybadger that reads 'Turn your logs into events.'

    "Splunk-like querying without having to sell my kidneys? nice"

    That’s a direct quote from someone who just saw Honeybadger Insights. It’s a bit like Papertrail or DataDog—but with just the good parts and a reasonable price tag.

    Best of all, Insights logging is available on our free tier as part of a comprehensive monitoring suite including error tracking, uptime monitoring, status pages, and more.

    Start logging for FREE
    Simple 5-minute setup — No credit card required