Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

Back to skills

Add Read Model

ASecurity

Build a complete read model in Protean - a projection populated by a projector that reacts to domain events. Covers single-aggregate projections (flattened view of one aggregate), cross-aggregate projections (combining data from multiple aggregates), and choosing between database-backed and cache-backed storage. Use when the user asks to "add a read model", "create a projection with projector", "build a query view", "add a CQRS read side", "create a denormalized view", "build a reporting view...

45 stars
0 votes
0 copies
0 views
Added 9/20/2026
developmentpythongoreacttestingapidatabase

Works with

api

Security Analysis

A100/100

Scanned 9/20/2026

Install to Claude Code

$npx -y skills add proteanhq/protean --skill add-read-model --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Add Read Model?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Add Read Model
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/proteanhq-add-read-model/badge)](https://www.skillsdirectory.com/skills/proteanhq-add-read-model)

More formats (shields.io, HTML) on the badges page.

Download Zip
Files
SKILL.md
---
name: add-read-model
description: Build a complete read model in Protean - a projection populated by a projector that reacts to domain events. Covers single-aggregate projections (flattened view of one aggregate), cross-aggregate projections (combining data from multiple aggregates), and choosing between database-backed and cache-backed storage. Use when the user asks to "add a read model", "create a projection with projector", "build a query view", "add a CQRS read side", "create a denormalized view", "build a reporting view", "add a dashboard query model", or when they need to combine data from multiple aggregates into one queryable structure.
license: Apache-2.0
compatibility: Requires Python 3.11+, protean framework
metadata:
  author: proteanhq
  version: "0.1"
  category: workflow
  composes: [projection, projector, event, aggregate]
---

# Add Read Model

A read model is the query side of CQRS. It consists of three parts working together:

1. **Projection** — A denormalized, query-optimized data structure with basic field types only
2. **Projector** — An event listener that populates and updates the projection
3. **Domain events** — The bridge between write-side aggregates and read-side projections

## What this creates

| Component | Purpose | Decorator |
|-----------|---------|-----------|
| Projection | Flat, queryable data structure | `@domain.projection` |
| Projector | Event-to-projection mapper | `@domain.projector(projector_for=..., aggregates=[...])` |
| Events | Carry state changes from aggregates | `@domain.event(part_of=...)` |
| Aggregate factory methods | Raise events on state changes | `raise_()` in aggregate methods |

## Information to gather

Before building a read model, understand:

- [ ] **What query does this serve?** — What data does the UI/API need? (e.g., "product listing with stock levels")
- [ ] **Which aggregates contribute data?** — Single aggregate or multiple? (determines projector configuration)
- [ ] **What events trigger updates?** — Which state changes should update the read model?
- [ ] **What fields are needed?** — Only basic types allowed: String, Integer, Float, Identifier, DateTime, Boolean, Text, Date
- [ ] **What storage is appropriate?** — Database (durable, queryable) or cache (fast, ephemeral)?

## Process

### Step 1: Define the projection

Create a flat data structure with only basic field types. Every projection needs at least one `identifier=True` field.

```python
from protean.fields import DateTime, Float, Identifier, Integer, String

@domain.projection
class ProductListing:
    product_id: Identifier(identifier=True, required=True)
    name: String(max_length=200, required=True)
    price: Float()
    stock_quantity: Integer(default=0)
    category_name: String()        # Flattened from Category aggregate
    last_updated: DateTime()
```

**Key rules for projections** (see [projection](../projection/SKILL.md)):
- Basic field types only — no Reference, HasMany, HasOne, or ValueObject
- At least one field with `identifier=True`
- Flatten nested data into basic fields
- Use `@domain.projection` decorator

### Step 2: Define the events

Events carry the data needed to populate the projection. Define them with `part_of` pointing to their source aggregate.

```python
@domain.event(part_of="Product")
class ProductAdded:
    product_id: Identifier(required=True)
    name: String(required=True)
    price: Float(required=True)
    stock_quantity: Integer(required=True)

@domain.event(part_of="Product")
class ProductPriceChanged:
    product_id: Identifier(required=True)
    new_price: Float(required=True)
```

### Step 3: Raise events from aggregates

Use factory methods or domain methods to raise events when state changes.

```python
@domain.aggregate
class Product:
    name: String(required=True)
    price: Float(required=True)
    stock_quantity: Integer(default=0)

    @classmethod
    def create(cls, name, price, stock_quantity=0):
        product = cls(name=name, price=price, stock_quantity=stock_quantity)
        product.raise_(ProductAdded(
            product_id=product.id,
            name=name,
            price=price,
            stock_quantity=stock_quantity,
        ))
        return product

    def update_price(self, new_price):
        self.price = new_price
        self.raise_(ProductPriceChanged(
            product_id=self.id,
            new_price=new_price,
        ))
```

### Step 4: Build the projector

The projector listens to events and updates the projection. Use `@on` decorator from `protean.core.projector`.

```python
from protean.core.projector import on

@domain.projector(projector_for=ProductListing, aggregates=[Product])
class ProductListingProjector:
    @on(ProductAdded)
    def on_product_added(self, event: ProductAdded):
        repo = domain.repository_for(ProductListing)
        listing = ProductListing(
            product_id=event.product_id,
            name=event.name,
            price=event.price,
            stock_quantity=event.stock_quantity,
        )
        repo.add(listing)

    @on(ProductPriceChanged)
    def on_price_changed(self, event: ProductPriceChanged):
        repo = domain.repository_for(ProductListing)
        listing = repo.get(event.product_id)
        listing.price = event.new_price
        repo.add(listing)
```

### Step 5: Enable synchronous processing (for testing)

```python
domain.config["event_processing"] = "sync"
```

In production, events are processed asynchronously. For testing and development, set `event_processing` to `"sync"` so projectors run immediately when events are raised.

## Single-aggregate vs cross-aggregate

| Aspect | Single-aggregate | Cross-aggregate |
|--------|-----------------|-----------------|
| **Projector config** | `aggregates=[Product]` | `aggregates=[User, Transaction]` |
| **Data source** | One aggregate's events | Multiple aggregates' events |
| **Complexity** | Straightforward mapping | Must handle ordering, missing records |
| **Example** | Product → ProductListing | User + Transaction → UserBalance |

### Cross-aggregate projector

When combining data from multiple aggregates, the projector listens to events from all contributing aggregates:

```python
@domain.projector(
    projector_for=UserBalance,
    aggregates=[User, Transaction],
)
class UserBalanceProjector:
    @on(UserRegistered)
    def on_user_registered(self, event):
        balance = UserBalance(user_id=event.user_id, name=event.name, balance=0.0)
        domain.repository_for(UserBalance).add(balance)

    @on(TransactionCompleted)
    def on_transaction_completed(self, event):
        balance = domain.repository_for(UserBalance).get(event.user_id)
        balance.balance += event.amount
        domain.repository_for(UserBalance).add(balance)
```

## Choosing storage

| Storage | Option | Best for |
|---------|--------|----------|
| **Database** | `@domain.projection` (default) | Durable data, complex queries, reporting |
| **Database (specific)** | `@domain.projection(provider="postgres")` | Explicit provider selection |
| **Cache** | `@domain.projection(cache="redis")` | Fast reads, session data, ephemeral views |

## Decision guide

```
Need a queryable view of domain data?
├── Data from one aggregate only
│   └── Single-aggregate projector
│       aggregates=[TheAggregate]
│
├── Data from multiple aggregates
│   └── Cross-aggregate projector
│       aggregates=[AggregateA, AggregateB]
│
├── Need fast, ephemeral reads?
│   └── Cache-backed projection
│       @domain.projection(cache="redis")
│
└── Need durable, queryable data?
    └── Database-backed projection (default)
        @domain.projection  or  @domain.projection(provider="postgres")
```

## Common mistakes

### Complex field types in a projection

```python
# Wrong! Projections are flat — no associations or value objects
@domain.projection
class ProductListing:
    product_id: Identifier(identifier=True)
    category = Reference(Category)   # No
    reviews = HasMany("Review")      # No
```

Instead: flatten into basic fields (`category_name: String()`, `review_count: Integer()`).

### Business logic in the projector

```python
# Wrong! Projector decides business rules
@on(OrderPlaced)
def on_order_placed(self, event):
    if event.total > 1000:           # rule leaks into the read side
        apply_vip_discount(event)
```

Instead: projectors only map event data into projection rows; keep rules on the write side.

### Handling create but not update

```python
# Wrong! Only the create event is handled — the projection goes stale
@on(ProductAdded)
def on_added(self, event): ...
# (no handler for ProductPriceChanged)
```

Instead: handle every event that changes the projected data, including updates (and deletes).

### Reading the aggregate instead of the projection

```python
# Wrong! The read path queries the write model
orders = domain.repository_for(Order).query.filter(status="placed")
```

Instead: query the projection (`domain.repository_for(ProductListing)` or `view_for`), which is shaped for the query.

### Forgetting sync processing in tests

```python
# Wrong! The projector never runs, so the projection stays empty
domain.process(command)  # async by default
assert domain.repository_for(ProductListing).get("P-1")  # fails
```

Instead: set `domain.config["event_processing"] = "sync"` in tests so projectors run immediately.

## Complete examples

- [Single-aggregate read model](assets/read_model_single_aggregate.py) — Product → ProductListing with create and update events
- [Cross-aggregate read model](assets/read_model_cross_aggregate.py) — User + Order → CustomerOrderSummary combining data from two aggregates
- [Read model with multiple events](assets/read_model_multiple_events.py) — Inventory tracking with stock adjustments, reservations, and releases

## Detailed references

- [Single-Aggregate Read Model](references/single-aggregate-read-model.md) — Step-by-step walkthrough
- [Cross-Aggregate Read Model](references/cross-aggregate-read-model.md) — Combining data from multiple aggregates
- [Choosing Storage](references/choosing-storage.md) — Database vs cache trade-offs

## Related skills

- [projection](../projection/SKILL.md) — Projection definition and field types
- [projector](../projector/SKILL.md) — Projector patterns and configuration
- [event](../event/SKILL.md) — Defining domain events
- [aggregate](../aggregate/SKILL.md) — Aggregates that raise events

Attribution

proteanhqproteanhq
View sourceMore from proteanhq →
SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

Related Skills

Browser Extension Developer

Use this skill when developing or maintaining browser extension code in the `browser/` directory, including Chrome/Firefox/Edge compatibility, content scripts, background scripts, or i18n updates.

281612 votes

Seo Optimizer

SEO optimization with keyword analysis, readability assessment, technical validation, content quality. Use for search rankings, blog posts, content audits, or encountering keyword density, readability scores, meta tags, schema markup errors.

2132 votes

Google Official Seo Guide

Official Google SEO guide covering search optimization, best practices, Search Console, crawling, indexing, and improving website search visibility based on official Google documentation

1862 votes

Tanstack Start

Build a full-stack TanStack Start app on Cloudflare Workers from scratch — SSR, file-based routing, server functions, D1+Drizzle, better-auth, Tailwind v4+shadcn/ui. Use whenever the user mentions TanStack Start, asks to scaffold a full-stack Cloudflare app with SSR, wants an SSR dashboard, or asks for a React 19 + Cloudflare Workers app with file-based routing and server functions — even if they don't name TanStack Start specifically. No template repo — Claude generates every file fresh per ...

9881 votes

Pentest

PTES-aligned adversarial security audit for backend, frontend, and mobile applications. Produces a CVSS-scored Hacker Report with verified PoCs and phased remediation.

5491 votes
View all in development →