Read Model Pattern
Read Model Pattern
Section titled “Read Model Pattern”Core Idea: Create denormalized, query-optimized projections from event streams, updated automatically as events occur.
Overview
Section titled “Overview”Read models are denormalized views of event-sourced data, optimized for specific query patterns. Instead of querying the event log directly, read models provide fast, indexed access to data shaped for UI needs.
The platform automatically keeps read models synchronized with events through @MapFromEvent decorators.
The Problem
Section titled “The Problem”Querying event logs directly is slow:
// Querying events directly - SLOWasync function getOrderSummary(orderId: string) { const events = await eventStore.getEventsByAggregate('Order', orderId);
// Replay all events to get current state const order = new Order(); events.forEach(event => order.applyEvent(event));
return { id: order.id, status: order.status, total: order.items.reduce((sum, item) => sum + item.price, 0) }; // 50-100ms for each query - too slow!}Why This Matters:
- Event replay slow for large aggregates
- Complex queries require joining events
- UI needs denormalized data
The Solution
Section titled “The Solution”Read models provide pre-computed, indexed views:
// Read model - optimized for queries@ReadModel({ tableName: 'order_summaries', schema: { order_id: 'uuid PRIMARY KEY', user_id: 'uuid NOT NULL', status: 'varchar(50) NOT NULL', total: 'decimal(10,2) NOT NULL', item_count: 'integer NOT NULL', created_at: 'timestamp NOT NULL' }})export class OrderSummaryReadModel { // Automatically creates record @MapFromEvent(OrderCreatedEvent) async onCreate(event: OrderCreatedEvent) { await this.insert({ order_id: event.orderId, user_id: event.userId, status: 'pending', total: event.total, item_count: event.items.length, created_at: event.occurredAt }); }
// Automatically updates record @MapFromEvent(OrderPaidEvent) async onPaid(event: OrderPaidEvent) { await this.update( { order_id: event.orderId }, { status: 'paid' } ); }}
// Query is fast - direct database lookup@QueryHandler(GetOrderSummaryQuery)export class GetOrderSummaryHandler { async handle(query: GetOrderSummaryQuery) { // Sub-10ms query on indexed table return await this.db.query( 'SELECT * FROM order_summaries WHERE order_id = $1', [query.orderId] ); }}Benefits
Section titled “Benefits”- Fast Queries: Sub-10ms with indexes
- Denormalized: Data shaped for UI
- Automatic Updates: Platform keeps synchronized
- Multiple Views: Different read models for different needs
Best Practices
Section titled “Best Practices”-
One Read Model Per Query Pattern
- Create specific read models for specific UIs
- Don’t try to make one read model serve all queries
-
Design for Queries
- Think about what UI needs
- Denormalize aggressively
-
Handle Event Replay
- Read models must support rebuilding from events
- Use catchup process for deployments