Skip to content

Read Models

Read Models are denormalized views optimized for queries, built from events via projections.

Purpose

  • Optimize for read performance
  • Provide query-friendly data structures
  • Support complex queries without joins

Example: Account Balance Read Model

typescript
interface AccountBalanceReadModel {
  accountId: string;
  balance: number;
  currency: string;
  lastTransactionAt: Date;
  transactionCount: number;
}

Read Model Table

sql
CREATE TABLE account_balances (
  account_id VARCHAR(255) PRIMARY KEY,
  balance BIGINT NOT NULL,
  currency VARCHAR(3) NOT NULL,
  last_transaction_at TIMESTAMP,
  transaction_count INT DEFAULT 0,
  updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_balance ON account_balances(balance);
CREATE INDEX idx_currency ON account_balances(currency);

Building Read Model

Projections populate the read model:

typescript
// Event → Read Model
eventBus.subscribe('MoneyDeposited', async (event) => {
  await db.query(`
    INSERT INTO account_balances (account_id, balance, currency, transaction_count, last_transaction_at)
    VALUES ($1, $2, 'USD', 1, NOW())
    ON CONFLICT (account_id)
    DO UPDATE SET
      balance = account_balances.balance + $2,
      transaction_count = account_balances.transaction_count + 1,
      last_transaction_at = NOW()
  `, [event.metadata.aggregateId, event.payload.amount]);
});

Querying Read Model

typescript
class AccountBalanceQuery {
  constructor(private readonly db: Database) {}

  async getBalance(accountId: string): Promise<AccountBalanceReadModel> {
    const result = await this.db.query(
      'SELECT * FROM account_balances WHERE account_id = $1',
      [accountId]
    );
    return result.rows[0];
  }

  async getHighBalanceAccounts(minBalance: number): Promise<AccountBalanceReadModel[]> {
    const result = await this.db.query(
      'SELECT * FROM account_balances WHERE balance >= $1 ORDER BY balance DESC',
      [minBalance]
    );
    return result.rows;
  }
}

Multiple Read Models

Different models for different queries:

typescript
// Model 1: Fast balance lookup
interface AccountBalance {
  accountId: string;
  balance: number;
}

// Model 2: Transaction history
interface TransactionHistory {
  accountId: string;
  transactions: Transaction[];
}

// Model 3: Analytics dashboard
interface AccountAnalytics {
  totalDeposits: number;
  totalWithdrawals: number;
  avgTransactionAmount: number;
}

Documentation will be expanded soon.

Released under the MIT License.