Laravel & Vue: Real-Time Notifications using Reverb, Redis, and TDD - 03
José Rafael Gutierrez
10 months ago
Chapter 3: Configuring Redis to Store and Manage Notifications
Objective
In this chapter, we will configure Redis to manage real-time notifications, synchronizing them with the database for long-term persistence. We will use the TDD cycle (red → green → refactor) to develop an efficient real-time notification system with Redis and robust persistence in the database.
1. Generate automatic notifications when the status of an order (Order) changes.
2. Store notifications in Redis with expiration times to improve performance.
3. Notify both the user and administrators about changes in the status of an order.
4. Implement a system to mark notifications as "read" or "unread" using Redis.
Introduction
In modern applications, Redis is an in-memory database that offers exceptional speed for read and write operations. This makes it an ideal solution for handling real-time notifications, where latency must be minimal. By combining Redis with a relational database like MySQL or PostgreSQL, we can get the best of both worlds: speed in delivering notifications and persistence to maintain a permanent history.
We will work with a new entity called Order, which represents an order with a delivery status (delivery_status). Every time the status of an order changes (for example, from "in preparation" to "in transit"), an Observer will be responsible for automatically generating notifications for both the user and the system administrators.
TDD Cycle: Generating Notifications
1. Write the Initial Test
We will start by writing a test to validate that notifications are generated when the status of an order changes and that these notifications are sent to both the user and the administrators.
Run the following command to create a test file:
sail artisan make:test OrderStatusObserverTest
Modify tests/Feature/OrderStatusObserverTest.php with the following content:
namespace Tests\Feature;
use App\Models\Notification;
use App\Models\Order;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Redis;
use Tests\TestCase;
class OrderStatusObserverTest extends TestCase
{
use RefreshDatabase;
public function test_it_creates_notifications_for_user_and_admins_on_order_status_change()
{
// Given
$user = User::factory()->create();
$admin = User::factory()->state(['is_admin' => true])->create();
// When
$order = Order::create([
'user_id' => $user->id,
'delivery_status' => 'in_preparation',
]);
$order->update(['delivery_status' => 'in_transit']);
// Then
$this->assertDatabaseHas('notifications', [
'user_id' => $user->id,
'message' => 'El estado del pedido ha cambiado a: En tránsito.',
]);
$this->assertDatabaseHas('notifications', [
'user_id' => $admin->id,
'message' => 'El pedido #1 ha cambiado de estado a: En tránsito.',
]);
$userNotification = json_decode(Redis::lpop("notifications:{$user->id}"), true);
$adminNotification = json_decode(Redis::lpop("notifications:{$admin->id}"), true);
$this->assertEquals('El estado del pedido ha cambiado a: En tránsito.', $userNotification['message']);
$this->assertEquals('El pedido #1 ha cambiado de estado a: En tránsito.', $adminNotification['message']);
}
}
2. Run the Initial Test (Red)
Run the test to confirm that it fails because neither the Order model nor its observer exist:
sail artisan test --filter=OrderStatusObserverTest
Expected Errors:
-
Orderclass not found. -
Migration for
ordersnot defined.
Now that we have identified the expected errors, let's create the Order model and its respective migration to solve the first failure.
Creation of Entities and Migrations
3. Create the Model and Migration for Order
Create the Order model and its migration with the following command:
sail artisan make:model Order -m
Modify the migration generated in database/migrations/xxxx_xx_xx_create_orders_table.php to include the table structure:
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('delivery_status');
$table->timestamps();
});
In the app/Models/Order.php model, add the fillable attribute:
protected $fillable = ['user_id', 'delivery_status'];
Run the migration:
sail artisan migrate
4. Create a New Migration to Add the message Field to notifications
To add the message field to the notifications table, run:
sail artisan make:migration add_message_to_notifications_table --table=notifications
Modify the migration generated in database/migrations/xxxx_xx_xx_add_message_to_notifications_table.php:
Schema::table('notifications', function (Blueprint $table) {
$table->text('message');
});
Run the migration:
sail artisan migrate
Modify the Notification model to include message in $fillable:
protected $fillable = ['user_id', 'status', 'message'];
Managing the Read Status of Notifications
To enhance the user experience, we will implement a functionality that allows marking notifications as "read" or "unread" using Redis.
Explanation of the Process:
-
When a notification is created, it is automatically added to the set of unread notifications in Redis.
-
When the user reviews a notification, it is removed from the unread set.
Code to Handle the Read Status
-
Add a Notification as Unread:
Redis::sadd("notifications:unread:{$user->id}", $notification->id); -
Mark a Notification as Read:
Redis::srem("notifications:unread:{$user->id}", $notificationId);
5. Implement the Observer to Generate Notifications (Green)
Create an Observer for the Order model:
sail artisan make:observer OrderObserver --model=Order
Modify app/Observers/OrderObserver.php with the logic to generate the notifications:
namespace App\Observers;
use App\Models\Notification;
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\Redis;
class OrderObserver
{
public function updating(Order $order)
{
if ($order->isDirty('delivery_status')) {
$status = $order->delivery_status;
// Message for the user
$userMessage = "El estado del pedido ha cambiado a: {$this->getStatusLabel($status)}.";
Notification::create([
'user_id' => $order->user_id,
'status' => $status,
'message' => $userMessage,
]);
Redis::lpush("notifications:{$order->user_id}", json_encode([
'message' => $userMessage,
'timestamp' => now()->timestamp,
]));
// Message for administrators
$adminMessage = "El pedido #{$order->id} ha cambiado de estado a: {$this->getStatusLabel($status)}.";
$admins = User::where('is_admin', true)->get();
foreach ($admins as $admin) {
Notification::create([
'user_id' => $admin->id,
'status' => $status,
'message' => $adminMessage,
]);
Redis::lpush("notifications:{$admin->id}", json_encode([
'message' => $adminMessage,
'timestamp' => now()->timestamp,
]));
}
}
}
private function getStatusLabel(string $status): string
{
return [
'in_preparation' => 'En preparación',
'in_transit' => 'En tránsito',
'delivered' => 'Entregado',
][$status] ?? $status;
}
}
Register the observer in AppServiceProvider:
use App\Models\Order;
use App\Observers\OrderObserver;
public function boot()
{
Order::observe(OrderObserver::class);
}
Now, run the test again:
sail artisan test --filter=OrderStatusObserverTest
PASS Tests\Feature\OrderStatusObserverTest
✓ it creates notifications for user and admins on order status change
Updating the NotificationTest Test
Why Update the Test?
After adding the message field to the notifications table, our original test started failing because it did not consider this new required field. Let's update the test to reflect this new data structure and comply with the TDD cycle.
1. Running the Test (Red)
If we run the following command to run all tests:
sail artisan test
We get the following error:
FAIL Tests\Feature\NotificationTest
⨯ it creates a notification
Failed asserting that a row in the table [notifications] matches the attributes {
"user_id": 1,
"status": "Enviado"
}.
Found similar results: [
{
"user_id": 1,
"status": "Enviado",
"message": null
}
].
Error Analysis:
The test fails because creating a notification now requires a non-null message. The migration we added sets the message field as a text, so we must include it when creating a notification in the test.
2. Modifying the Test (Green)
To solve the error, we will update the NotificationTest test to include the message field when creating a notification.
Modify the tests/Feature/NotificationTest.php file as follows:
<?php
namespace Tests\Feature;
use App\Models\Notification;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class NotificationTest extends TestCase
{
use RefreshDatabase;
public function test_it_creates_a_notification()
{
// Given
$user = User::factory()->create();
$data = [
'user_id' => $user->id,
'status' => 'Enviado',
'message' => 'El pedido ha sido enviado',
];
// When
Notification::create($data);
// Then
$this->assertDatabaseHas('notifications', $data);
}
}
3. Running the Corrected Test
Now, run the test again:
sail artisan test --filter=NotificationTest
The test should pass correctly:
PASS Tests\Feature\NotificationTest
✓ it creates a notification
4. Now we run all the tests
sail artisan test

Explanation of the Changes
-
Addition of the
messageField: Themessagefield was added to the$dataarray to comply with the new requirement of theNotificationmodel. -
Consistency with the Migration: The
messagefield is mandatory due to the migration we added. This update to the test ensures that we are creating valid notifications that meet the current requirements of the model and the database. -
Application of the TDD Cycle:
-
Red: The test failed after adding the
messagefield. -
Green: We updated the test to include the
messagefield, and the test passed. - Refactor: No additional refactoring is required in this case, as the code is clear and concise.
-
Red: The test failed after adding the
Conclusion
In this chapter, we implemented a complete notification system using Redis and Laravel, following the TDD cycle. We have achieved:
- Generating automatic notifications when the status of an order changes.
- Storing notifications in Redis to improve performance.
- Synchronizing with the database for long-term persistence.
- Managing the "read" or "unread" status with Redis.
In the next chapter, we’ll implement a more flexible notification system using Laravel’s events, listeners, and queues to decouple logic and improve application performance. 🚀