Laravel Read Model Projections Without Event Sourcing | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Read Model Projections in Laravel Without Full Event Sourcing        On this page       1. [  The Problem With Querying Your Write Model ](#the-problem-with-querying-your-write-model)
2. [  Anatomy of a Lightweight Projector ](#anatomy-of-a-lightweight-projector)
3. [  Designing the Read Model Table ](#designing-the-read-model-table)
4. [  Handling Updates and Deletions ](#handling-updates-and-deletions)
5. [  Rebuilding a Projection ](#rebuilding-a-projection)
6. [  Querying the Read Model ](#querying-the-read-model)
7. [  Key Takeaways ](#key-takeaways)

  ![Read Model Projections in Laravel Without Full Event Sourcing](https://cdn.msaied.com/220/967a0feff7428e3f2a68209163804743.png)

  #laravel   #architecture   #eloquent   #cqrs  

 Read Model Projections in Laravel Without Full Event Sourcing 
===============================================================

     17 Jun 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   The Problem With Querying Your Write Model  ](#the-problem-with-querying-your-write-model)
2. [  02   Anatomy of a Lightweight Projector  ](#anatomy-of-a-lightweight-projector)
3. [  03   Designing the Read Model Table  ](#designing-the-read-model-table)
4. [  04   Handling Updates and Deletions  ](#handling-updates-and-deletions)
5. [  05   Rebuilding a Projection  ](#rebuilding-a-projection)
6. [  06   Querying the Read Model  ](#querying-the-read-model)
7. [  07   Key Takeaways  ](#key-takeaways)

 The Problem With Querying Your Write Model
------------------------------------------

Every sufficiently complex Laravel app eventually develops the same symptom: a single Eloquent model doing too much. It holds business logic, fires observers, and is also the thing you query for reports, dashboards, and API responses. The joins pile up, scopes multiply, and `EXPLAIN` starts returning rows you'd rather not see.

Full event sourcing solves this cleanly—but it's a significant architectural commitment. You don't always need it. What you often *do* need is a dedicated **read model**: a denormalised, query-optimised table (or set of tables) that is kept in sync with your write side through lightweight projectors.

This article shows you how to build that pattern in plain Laravel without pulling in Spatie's event-sourcing package or rebuilding your entire domain.

---

Anatomy of a Lightweight Projector
----------------------------------

A projector is just a class that listens to domain events and updates a read model. In Laravel, that maps naturally to event listeners.

```php
// app/Events/OrderPlaced.php
final class OrderPlaced
{
    public function __construct(
        public readonly Order $order,
    ) {}
}

```

```php
// app/Projectors/OrderSummaryProjector.php
final class OrderSummaryProjector
{
    public function handle(OrderPlaced $event): void
    {
        OrderSummary::create([
            'order_id'    => $event->order->id,
            'customer_id' => $event->order->customer_id,
            'total_cents' => $event->order->total_cents,
            'status'      => $event->order->status->value,
            'placed_at'   => $event->order->created_at,
        ]);
    }
}

```

Register it in `EventServiceProvider` (or the `#[AsEventListener]` attribute in Laravel 11+):

```php
protected $listen = [
    OrderPlaced::class => [
        OrderSummaryProjector::class,
    ],
];

```

The `order_summaries` table is your read model. It has exactly the columns your dashboard query needs—no joins, no computed attributes, no `withCount`.

---

Designing the Read Model Table
------------------------------

The read model schema should be shaped by the *query*, not the domain entity.

```php
Schema::create('order_summaries', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('order_id')->unique();
    $table->unsignedBigInteger('customer_id')->index();
    $table->unsignedBigInteger('total_cents');
    $table->string('status', 32)->index();
    $table->timestamp('placed_at')->index();
    $table->timestamps();
});

```

Notice there's no foreign key constraint to `orders`. The read model is **eventually consistent** by design; referential integrity is the write side's concern.

---

Handling Updates and Deletions
------------------------------

Projectors must handle the full lifecycle:

```php
public function handleStatusChanged(OrderStatusChanged $event): void
{
    OrderSummary::where('order_id', $event->orderId)
        ->update(['status' => $event->newStatus->value]);
}

public function handleCancelled(OrderCancelled $event): void
{
    OrderSummary::where('order_id', $event->orderId)->delete();
}

```

Group all projector methods in one class per read model. This keeps the projection logic co-located and easy to reason about.

---

Rebuilding a Projection
-----------------------

One of the underrated benefits of this pattern is that you can **rebuild** a read model from scratch by replaying your write-side data. Create an Artisan command:

```php
final class RebuildOrderSummaries extends Command
{
    protected $signature = 'projections:rebuild-order-summaries';

    public function handle(): void
    {
        OrderSummary::truncate();

        Order::with('customer')->lazyById(500)->each(function (Order $order) {
            OrderSummary::create([
                'order_id'    => $order->id,
                'customer_id' => $order->customer_id,
                'total_cents' => $order->total_cents,
                'status'      => $order->status->value,
                'placed_at'   => $order->created_at,
            ]);
        });

        $this->info('Done.');
    }
}

```

Using `lazyById` keeps memory flat regardless of table size.

---

Querying the Read Model
-----------------------

Your controller or query class now hits a single, indexed table:

```php
final class OrderDashboardQuery
{
    public function execute(int $customerId): Collection
    {
        return OrderSummary::where('customer_id', $customerId)
            ->where('status', '!=', 'cancelled')
            ->orderByDesc('placed_at')
            ->limit(50)
            ->get();
    }
}

```

No joins. No `with()`. No N+1 risk. The query plan is trivially predictable.

---

Key Takeaways
-------------

- **Read models decouple query shape from domain shape**—design them for the consumer, not the entity.
- **Projectors are just event listeners**; no new infrastructure required.
- **Eventual consistency is acceptable** for most dashboard and reporting use cases.
- **Rebuild commands** give you a safety net when projection logic changes.
- **`lazyById` + chunked iteration** keeps rebuild memory usage flat at any scale.
- Start with one read model for your most painful query; you don't need to project everything.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fread-model-projections-in-laravel-without-full-event-sourcing&text=Read+Model+Projections+in+Laravel+Without+Full+Event+Sourcing) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fread-model-projections-in-laravel-without-full-event-sourcing) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Does this pattern require event sourcing or a dedicated event store?        No. The projectors listen to standard Laravel events dispatched from your service layer or Eloquent observers. Your write-side data stays in normal relational tables. The read model is just an additional denormalised table kept in sync via those events. 

      Q02  What happens if an event is missed and the read model goes out of sync?        That's exactly why the rebuild command exists. You can truncate and repopulate the read model from the authoritative write-side tables at any time. For critical projections, consider running the rebuild on a schedule or adding a reconciliation job that compares counts between the two sides. 

      Q03  Should projectors run synchronously or be queued?        For low-latency dashboards, synchronous listeners are simplest. If the projection update is expensive or the event is fired inside a database transaction, implement ShouldQueue on the listener so the projection update happens after the transaction commits, avoiding phantom reads. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Laravel Queues at Scale: Backpressure, Overflow Routing, and Dead-Letter Strategies](https://cdn.msaied.com/221/40080cae015b9b54cba4e665a16dbe7e.png) laravel queues reliability 

### Laravel Queues at Scale: Backpressure, Overflow Routing, and Dead-Letter Strategies

Beyond basic queue configuration lies a set of production patterns—backpressure detection, overflow routing to...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 17 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-queues-at-scale-backpressure-overflow-routing-and-dead-letter-strategies) [ ![Laravel Observers vs. Model Events: Choosing the Right Hook for Side Effects](https://cdn.msaied.com/219/c16eb6ef2428da6f1d4a18025bf6c15d.png) laravel eloquent observers 

### Laravel Observers vs. Model Events: Choosing the Right Hook for Side Effects

Model events and observers both react to Eloquent lifecycle changes, but picking the wrong one creates tangled...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 16 Jun 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-observers-vs-model-events-choosing-the-right-hook-for-side-effects) [ ![Laravel Telescope Alternatives: Building a Lightweight Request Inspector with Middleware](https://cdn.msaied.com/216/9b6d240010b80483f072902dafcd216c.png) laravel middleware debugging 

### Laravel Telescope Alternatives: Building a Lightweight Request Inspector with Middleware

Telescope is powerful but heavy for production. Learn how to build a focused, low-overhead request inspector u...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 16 Jun 2026     1 min read  

  Read    

 ](https://msaied.com/articles/laravel-telescope-alternatives-building-a-lightweight-request-inspector-with-middleware) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
