Mahamudul Hasan Rubel
HomeAboutProjectsSkillsExperienceBlogPhotosContact
Mahamudul Hasan Rubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • About
  • Projects
  • Skills
  • Experience
  • Blog
  • Photos
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
LaravelPHPJune 22, 20264 min read

Laravel CQRS: Implementing Versioned State Snapshots for Consistency

Master Laravel CQRS and eventual consistency using versioned state snapshots. Learn how to keep your read models deterministic and reliable at scale.

LaravelCQRSEventual ConsistencyDomain EventsState SnapshotsArchitecturePHPBackend

Last month, I spent three days debugging a race condition where our read models were showing stale data for nearly 400ms after a critical transaction. When you move away from a monolithic CRUD approach into a decoupled architecture, you quickly realize that the gap between a command execution and a projection update is where your system’s reliability goes to die.

The Challenge of Eventual Consistency in CQRS

In a standard Laravel CQRS setup, your write model emits domain events, which a queue worker eventually consumes to update the read model. The problem is that the user often triggers a read immediately after the command returns. If the Domain Events haven't been processed yet, the UI reflects the "old" state, leading to user confusion and support tickets.

We initially tried forcing synchronous projections for critical paths. That was a mistake. It spiked our request latency and made the write model fragile. If the projection failed, the entire user request failed. We needed a way to guarantee consistency without sacrificing the performance benefits of asynchronous processing.

Implementing Versioned State Snapshots

The solution isn't to make everything synchronous, but to make the read model aware of the "version" it currently holds compared to the write model. By attaching a version number (or a sequence ID) to your aggregates, you can track the progress of your projections.

When you execute a command, you increment the version on the aggregate root. That version travels with the domain event. When your projection handler receives the event, it updates the read model and stores the latest version ID.

PHP
#6A9955">// Inside your Aggregate Root
public function apply(OrderPlaced $event): void
{
    $this->version++;
    $this->state = $event->state;
}

#6A9955">// Inside your Projection Handler
public function handle(OrderPlaced $event): void
{
    DB::table('order_read_models')->updateOrInsert(
        ['id' => $event->orderId],
        [
            'data' => json_encode($event->payload),
            'version' => $event->version,
            'updated_at' => now(),
        ]
    );
}

Achieving Deterministic State

To ensure the client sees consistent data, you can implement a "version-check" header in your API responses. When the write model processes a request, it returns the new version number. The frontend stores this. On subsequent reads, the client sends this version in the header.

If the read model’s version is lower than the requested version, you have two choices:

  1. Poll: Wait a few milliseconds and retry.
  2. Read-through: Fetch the state directly from the write model (or an event store) if the read model is lagging.

Using Database caching: Implementing Redis Write-Through for Consistency can help here. You can store the "latest version" for a specific aggregate in Redis. Your read model API can check this key to decide if it needs to serve data or trigger a "please wait" state.

Why Versioned State Snapshots Work

By tracking versioned state snapshots, you decouple the eventual nature of the background processing from the immediate needs of the user. You aren't forcing the write model to wait; you're providing the read model with the context it needs to report its own readiness.

I’ve found that keeping a last_processed_version column on the read model is essentially free in terms of performance. It adds about 2-3 bytes to your row size but provides the observability you need to detect when your queues are falling behind. When combined with Laravel Read-Write Splitting: Deterministic Connection Routing Guide, you can ensure that your read models are always pulling from a consistent replica without hitting the primary database.

Trade-offs and Caveats

This approach isn't a silver bullet. You’re essentially introducing state management complexity to solve a concurrency problem. If your aggregate root generates events at a massive scale, your version increments become a bottleneck on the primary write database.

I’m still experimenting with how to handle "gaps" in versions. If a projection fails and skips a version, you need a recovery strategy—usually a replay of events from the event store. It’s not trivial, but it’s the price of admission for a truly decoupled, scalable system. If you’re just building a standard CRUD app, don't over-engineer this. But if your system relies on Laravel Workflow: Architecting Asynchronous State Machines for Reliability to handle complex business processes, versioning is non-negotiable.

What I'd do differently next time? I'd start with a simple sequence counter in the database rather than trying to build a full-blown event store from day one. It’s easier to debug and gets you 90% of the way there.

Back to Blog

Similar Posts

LaravelPHPJune 21, 20265 min read

Laravel Symfony Messenger: Architecting Resilient Domain Events

Laravel Symfony Messenger integration allows you to build resilient domain events. Learn to decouple your architecture and handle background tasks reliably.

Read more
LaravelPHP
June 22, 2026
4 min read

Laravel Queues and Fork-Join Pattern: Parallel Processing Strategies

Master Laravel Queues and the Fork-Join pattern. Learn to implement parallel processing with Redis Lua scripting for atomic task decomposition and scaling.

Read more
LaravelPHPJune 22, 20264 min read

Laravel Horizon Idempotency: Building Deterministic Redis Task Keys

Master Laravel Horizon idempotency by implementing content-addressable task keys and Redis deduplication for exactly-once processing in your background jobs.

Read more