Read Model Projections
Also Known As
projection
read model
CQRS read side
event projection
TL;DR
Denormalised views of domain data built by processing event streams — the read side of CQRS, optimised for query performance rather than write consistency.
Explanation
In CQRS/Event Sourcing, the write model stores domain events. Projections listen to those events and build optimised read models — denormalised, pre-computed, shaped exactly for query needs. A dashboard projection might aggregate order totals per customer; a search projection might build Elasticsearch documents; a reporting projection might maintain monthly revenue. Projections can be rebuilt by replaying all events. Each projection is independent — one event can feed many projections without coupling.
Diagram
flowchart LR
CMD[Command] --> AGG[Aggregate]
AGG --> STORE[(Event Store<br/>OrderPlaced<br/>PaymentReceived)]
STORE -->|project| P1[CustomerStats<br/>projection]
STORE -->|project| P2[DashboardSummary<br/>projection]
STORE -->|project| P3[SearchIndex<br/>projection]
P1 --> R1[(Redis<br/>O of 1 lookup)]
P2 --> R2[(Postgres<br/>pre-aggregated)]
P3 --> R3[(Elasticsearch)]
style STORE fill:#6e40c9,color:#fff
style R1 fill:#238636,color:#fff
style R2 fill:#238636,color:#fff
style R3 fill:#1f6feb,color:#fff
Common Misconception
✗ Projections are just database views — views query normalised data at read time; projections denormalise proactively by processing each event as it occurs, making reads O(1) instead of O(joins).
Why It Matters
A read model projection for 'customer order history' stores a pre-computed array on the customer document — the query is a single key lookup instead of joining orders, line items, and products at read time.
Common Mistakes
- Projections that query the write model — projections should be fed by events, not queries.
- Not making projections idempotent — event replays must produce the same result; use event IDs to detect re-processing.
- One monolithic projection — separate projections for each use case, each independently rebuildable.
- Not handling projection failures — failed projections cause stale read models; monitor projection lag.
Code Examples
✗ Vulnerable
// Slow read — joins at query time:
SELECT c.name, SUM(o.total) as lifetime_value,
COUNT(o.id) as order_count,
MAX(o.created_at) as last_order
FROM customers c
JOIN orders o ON o.customer_id = c.id
GROUP BY c.id;
-- Full table scan + aggregation on every dashboard load
✓ Fixed
// Projection updated on each OrderPlaced event:
class CustomerStatsProjection {
public function onOrderPlaced(OrderPlaced $event): void {
$stats = $this->store->find($event->customerId) ?? [
'order_count' => 0, 'lifetime_value' => 0, 'last_order' => null
];
$stats['order_count']++;
$stats['lifetime_value'] += $event->total;
$stats['last_order'] = $event->occurredAt;
$this->store->save($event->customerId, $stats);
}
}
// Dashboard query: single key lookup — O(1) regardless of order count
References
Tags
🤝 Adopt this term
£79/year · your link shown here
Added
16 Mar 2026
Edited
22 Mar 2026
Views
37
🤖 AI Guestbook educational data only
|
|
Last 30 days
Agents 2
Amazonbot 1
No pings yesterday
Amazonbot 9
Google 7
Perplexity 7
ChatGPT 2
Ahrefs 2
Unknown AI 2
Also referenced
How they use it
crawler 28
crawler_json 1
Related categories
⚡
DEV INTEL
Tools & Severity
🟡 Medium
⚙ Fix effort: High
⚡ Quick Fix
Build a dedicated read model for complex query needs — project domain events into a denormalised table or document tuned exactly for what the UI needs, not what's convenient to store
📦 Applies To
any
web
cli
queue-worker
🔍 Detection Hints
Complex JOIN query serving a read use case that could be a denormalised read model; same domain model used for both writes and complex reporting reads
Auto-detectable:
✗ No
phpstan
deptrac
⚠ Related Problems
🤖 AI Agent
Confidence: Low
False Positives: Medium
✗ Manual fix
Fix: High
Context: File
Tests: Update