[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M70FZBSNA04F737EBSTCQP8D":3,"skill-same:01M70FZBSNA04F737EBSTCQP8D":63},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":34,"linkOnly":35,"markdownContent":36,"markdownBody":37,"frontMatter":38,"refName":39,"commitSha":40,"skillDirectory":41,"forksCount":42,"repositoryHtmlUrl":43,"repositoryHomepageUrl":44,"ownerHtmlUrl":45,"ownerBlogUrl":46,"ownerDescription":47,"updatedAt":48,"install":49,"resources":62},"01M70FZBSNA04F737EBSTCQP8D","projection-patterns",null,"Build read models and projections from event streams. Use when implementing CQRS read sides, building materialized views, or optimizing query performance in event-sourced systems.","shengdabai\u002FTony-Claude-Code-Skills","shengdabai","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F116417419?v=4","01M70F6D2WY5PQV5A92BKYJQEW","Tony Sheng","01M70F6D3GAAAYZ5H4CC1G8VXM",3,"MIT","https:\u002F\u002Fgithub.com\u002Fshengdabai\u002FTony-Claude-Code-Skills\u002Fblob\u002Fmain\u002Fskills\u002F_collections\u002Fplugins\u002Fbackend-development\u002Fskills\u002Fprojection-patterns\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33],"projection patterns","patterns","event","performance","projections","implementing","streams","use","when","query","cqrs","build","models","read","systems","2026-11-10 10:12:30",false,"---\nname: projection-patterns\ndescription: Build read models and projections from event streams. Use when implementing CQRS read sides, building materialized views, or optimizing query performance in event-sourced systems.\n---\n\n# Projection Patterns\n\nComprehensive guide to building projections and read models for event-sourced systems.\n\n## When to Use This Skill\n\n- Building CQRS read models\n- Creating materialized views from events\n- Optimizing query performance\n- Implementing real-time dashboards\n- Building search indexes from events\n- Aggregating data across streams\n\n## Core Concepts\n\n### 1. Projection Architecture\n\n```\n┌─────────────┐     ┌─────────────┐     ┌─────────────┐\n│ Event Store │────►│ Projector   │────►│ Read Model  │\n│             │     │             │     │ (Database)  │\n│ ┌─────────┐ │     │ ┌─────────┐ │     │ ┌─────────┐ │\n│ │ Events  │ │     │ │ Handler │ │     │ │ Tables  │ │\n│ └─────────┘ │     │ │ Logic   │ │     │ │ Views   │ │\n│             │     │ └─────────┘ │     │ │ Cache   │ │\n└─────────────┘     └─────────────┘     └─────────────┘\n```\n\n### 2. Projection Types\n\n| Type | Description | Use Case |\n|------|-------------|----------|\n| **Live** | Real-time from subscription | Current state queries |\n| **Catchup** | Process historical events | Rebuilding read models |\n| **Persistent** | Stores checkpoint | Resume after restart |\n| **Inline** | Same transaction as write | Strong consistency |\n\n## Templates\n\n### Template 1: Basic Projector\n\n```python\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass\nfrom typing import Dict, Any, Callable, List\nimport asyncpg\n\n@dataclass\nclass Event:\n    stream_id: str\n    event_type: str\n    data: dict\n    version: int\n    global_position: int\n\n\nclass Projection(ABC):\n    \"\"\"Base class for projections.\"\"\"\n\n    @property\n    @abstractmethod\n    def name(self) -> str:\n        \"\"\"Unique projection name for checkpointing.\"\"\"\n        pass\n\n    @abstractmethod\n    def handles(self) -> List[str]:\n        \"\"\"List of event types this projection handles.\"\"\"\n        pass\n\n    @abstractmethod\n    async def apply(self, event: Event) -> None:\n        \"\"\"Apply event to the read model.\"\"\"\n        pass\n\n\nclass Projector:\n    \"\"\"Runs projections from event store.\"\"\"\n\n    def __init__(self, event_store, checkpoint_store):\n        self.event_store = event_store\n        self.checkpoint_store = checkpoint_store\n        self.projections: List[Projection] = []\n\n    def register(self, projection: Projection):\n        self.projections.append(projection)\n\n    async def run(self, batch_size: int = 100):\n        \"\"\"Run all projections continuously.\"\"\"\n        while True:\n            for projection in self.projections:\n                await self._run_projection(projection, batch_size)\n            await asyncio.sleep(0.1)\n\n    async def _run_projection(self, projection: Projection, batch_size: int):\n        checkpoint = await self.checkpoint_store.get(projection.name)\n        position = checkpoint or 0\n\n        events = await self.event_store.read_all(position, batch_size)\n\n        for event in events:\n            if event.event_type in projection.handles():\n                await projection.apply(event)\n\n            await self.checkpoint_store.save(\n                projection.name,\n                event.global_position\n            )\n\n    async def rebuild(self, projection: Projection):\n        \"\"\"Rebuild a projection from scratch.\"\"\"\n        await self.checkpoint_store.delete(projection.name)\n        # Optionally clear read model tables\n        await self._run_projection(projection, batch_size=1000)\n```\n\n### Template 2: Order Summary Projection\n\n```python\nclass OrderSummaryProjection(Projection):\n    \"\"\"Projects order events to a summary read model.\"\"\"\n\n    def __init__(self, db_pool: asyncpg.Pool):\n        self.pool = db_pool\n\n    @property\n    def name(self) -> str:\n        return \"order_summary\"\n\n    def handles(self) -> List[str]:\n        return [\n            \"OrderCreated\",\n            \"OrderItemAdded\",\n            \"OrderItemRemoved\",\n            \"OrderShipped\",\n            \"OrderCompleted\",\n            \"OrderCancelled\"\n        ]\n\n    async def apply(self, event: Event) -> None:\n        handlers = {\n            \"OrderCreated\": self._handle_created,\n            \"OrderItemAdded\": self._handle_item_added,\n            \"OrderItemRemoved\": self._handle_item_removed,\n            \"OrderShipped\": self._handle_shipped,\n            \"OrderCompleted\": self._handle_completed,\n            \"OrderCancelled\": self._handle_cancelled,\n        }\n\n        handler = handlers.get(event.event_type)\n        if handler:\n            await handler(event)\n\n    async def _handle_created(self, event: Event):\n        async with self.pool.acquire() as conn:\n            await conn.execute(\n                \"\"\"\n                INSERT INTO order_summaries\n                (order_id, customer_id, status, total_amount, item_count, created_at)\n                VALUES ($1, $2, $3, $4, $5, $6)\n                \"\"\",\n                event.data['order_id'],\n                event.data['customer_id'],\n                'pending',\n                0,\n                0,\n                event.data['created_at']\n            )\n\n    async def _handle_item_added(self, event: Event):\n        async with self.pool.acquire() as conn:\n            await conn.execute(\n                \"\"\"\n                UPDATE order_summaries\n                SET total_amount = total_amount + $2,\n                    item_count = item_count + 1,\n                    updated_at = NOW()\n                WHERE order_id = $1\n                \"\"\",\n                event.data['order_id'],\n                event.data['price'] * event.data['quantity']\n            )\n\n    async def _handle_item_removed(self, event: Event):\n        async with self.pool.acquire() as conn:\n            await conn.execute(\n                \"\"\"\n                UPDATE order_summaries\n                SET total_amount = total_amount - $2,\n                    item_count = item_count - 1,\n                    updated_at = NOW()\n                WHERE order_id = $1\n                \"\"\",\n                event.data['order_id'],\n                event.data['price'] * event.data['quantity']\n            )\n\n    async def _handle_shipped(self, event: Event):\n        async with self.pool.acquire() as conn:\n            await conn.execute(\n                \"\"\"\n                UPDATE order_summaries\n                SET status = 'shipped',\n                    shipped_at = $2,\n                    updated_at = NOW()\n                WHERE order_id = $1\n                \"\"\",\n                event.data['order_id'],\n                event.data['shipped_at']\n            )\n\n    async def _handle_completed(self, event: Event):\n        async with self.pool.acquire() as conn:\n            await conn.execute(\n                \"\"\"\n                UPDATE order_summaries\n                SET status = 'completed',\n                    completed_at = $2,\n                    updated_at = NOW()\n                WHERE order_id = $1\n                \"\"\",\n                event.data['order_id'],\n                event.data['completed_at']\n            )\n\n    async def _handle_cancelled(self, event: Event):\n        async with self.pool.acquire() as conn:\n            await conn.execute(\n                \"\"\"\n                UPDATE order_summaries\n                SET status = 'cancelled',\n                    cancelled_at = $2,\n                    cancellation_reason = $3,\n                    updated_at = NOW()\n                WHERE order_id = $1\n                \"\"\",\n                event.data['order_id'],\n                event.data['cancelled_at'],\n                event.data.get('reason')\n            )\n```\n\n### Template 3: Elasticsearch Search Projection\n\n```python\nfrom elasticsearch import AsyncElasticsearch\n\nclass ProductSearchProjection(Projection):\n    \"\"\"Projects product events to Elasticsearch for full-text search.\"\"\"\n\n    def __init__(self, es_client: AsyncElasticsearch):\n        self.es = es_client\n        self.index = \"products\"\n\n    @property\n    def name(self) -> str:\n        return \"product_search\"\n\n    def handles(self) -> List[str]:\n        return [\n            \"ProductCreated\",\n            \"ProductUpdated\",\n            \"ProductPriceChanged\",\n            \"ProductDeleted\"\n        ]\n\n    async def apply(self, event: Event) -> None:\n        if event.event_type == \"ProductCreated\":\n            await self.es.index(\n                index=self.index,\n                id=event.data['product_id'],\n                document={\n                    'name': event.data['name'],\n                    'description': event.data['description'],\n                    'category': event.data['category'],\n                    'price': event.data['price'],\n                    'tags': event.data.get('tags', []),\n                    'created_at': event.data['created_at']\n                }\n            )\n\n        elif event.event_type == \"ProductUpdated\":\n            await self.es.update(\n                index=self.index,\n                id=event.data['product_id'],\n                doc={\n                    'name': event.data['name'],\n                    'description': event.data['description'],\n                    'category': event.data['category'],\n                    'tags': event.data.get('tags', []),\n                    'updated_at': event.data['updated_at']\n                }\n            )\n\n        elif event.event_type == \"ProductPriceChanged\":\n            await self.es.update(\n                index=self.index,\n                id=event.data['product_id'],\n                doc={\n                    'price': event.data['new_price'],\n                    'price_updated_at': event.data['changed_at']\n                }\n            )\n\n        elif event.event_type == \"ProductDeleted\":\n            await self.es.delete(\n                index=self.index,\n                id=event.data['product_id']\n            )\n```\n\n### Template 4: Aggregating Projection\n\n```python\nclass DailySalesProjection(Projection):\n    \"\"\"Aggregates sales data by day for reporting.\"\"\"\n\n    def __init__(self, db_pool: asyncpg.Pool):\n        self.pool = db_pool\n\n    @property\n    def name(self) -> str:\n        return \"daily_sales\"\n\n    def handles(self) -> List[str]:\n        return [\"OrderCompleted\", \"OrderRefunded\"]\n\n    async def apply(self, event: Event) -> None:\n        if event.event_type == \"OrderCompleted\":\n            await self._increment_sales(event)\n        elif event.event_type == \"OrderRefunded\":\n            await self._decrement_sales(event)\n\n    async def _increment_sales(self, event: Event):\n        date = event.data['completed_at'][:10]  # YYYY-MM-DD\n        async with self.pool.acquire() as conn:\n            await conn.execute(\n                \"\"\"\n                INSERT INTO daily_sales (date, total_orders, total_revenue, total_items)\n                VALUES ($1, 1, $2, $3)\n                ON CONFLICT (date) DO UPDATE SET\n                    total_orders = daily_sales.total_orders + 1,\n                    total_revenue = daily_sales.total_revenue + $2,\n                    total_items = daily_sales.total_items + $3,\n                    updated_at = NOW()\n                \"\"\",\n                date,\n                event.data['total_amount'],\n                event.data['item_count']\n            )\n\n    async def _decrement_sales(self, event: Event):\n        date = event.data['original_completed_at'][:10]\n        async with self.pool.acquire() as conn:\n            await conn.execute(\n                \"\"\"\n                UPDATE daily_sales SET\n                    total_orders = total_orders - 1,\n                    total_revenue = total_revenue - $2,\n                    total_refunds = total_refunds + $2,\n                    updated_at = NOW()\n                WHERE date = $1\n                \"\"\",\n                date,\n                event.data['refund_amount']\n            )\n```\n\n### Template 5: Multi-Table Projection\n\n```python\nclass CustomerActivityProjection(Projection):\n    \"\"\"Projects customer activity across multiple tables.\"\"\"\n\n    def __init__(self, db_pool: asyncpg.Pool):\n        self.pool = db_pool\n\n    @property\n    def name(self) -> str:\n        return \"customer_activity\"\n\n    def handles(self) -> List[str]:\n        return [\n            \"CustomerCreated\",\n            \"OrderCompleted\",\n            \"ReviewSubmitted\",\n            \"CustomerTierChanged\"\n        ]\n\n    async def apply(self, event: Event) -> None:\n        async with self.pool.acquire() as conn:\n            async with conn.transaction():\n                if event.event_type == \"CustomerCreated\":\n                    # Insert into customers table\n                    await conn.execute(\n                        \"\"\"\n                        INSERT INTO customers (customer_id, email, name, tier, created_at)\n                        VALUES ($1, $2, $3, 'bronze', $4)\n                        \"\"\",\n                        event.data['customer_id'],\n                        event.data['email'],\n                        event.data['name'],\n                        event.data['created_at']\n                    )\n                    # Initialize activity summary\n                    await conn.execute(\n                        \"\"\"\n                        INSERT INTO customer_activity_summary\n                        (customer_id, total_orders, total_spent, total_reviews)\n                        VALUES ($1, 0, 0, 0)\n                        \"\"\",\n                        event.data['customer_id']\n                    )\n\n                elif event.event_type == \"OrderCompleted\":\n                    # Update activity summary\n                    await conn.execute(\n                        \"\"\"\n                        UPDATE customer_activity_summary SET\n                            total_orders = total_orders + 1,\n                            total_spent = total_spent + $2,\n                            last_order_at = $3\n                        WHERE customer_id = $1\n                        \"\"\",\n                        event.data['customer_id'],\n                        event.data['total_amount'],\n                        event.data['completed_at']\n                    )\n                    # Insert into order history\n                    await conn.execute(\n                        \"\"\"\n                        INSERT INTO customer_order_history\n                        (customer_id, order_id, amount, completed_at)\n                        VALUES ($1, $2, $3, $4)\n                        \"\"\",\n                        event.data['customer_id'],\n                        event.data['order_id'],\n                        event.data['total_amount'],\n                        event.data['completed_at']\n                    )\n\n                elif event.event_type == \"ReviewSubmitted\":\n                    await conn.execute(\n                        \"\"\"\n                        UPDATE customer_activity_summary SET\n                            total_reviews = total_reviews + 1,\n                            last_review_at = $2\n                        WHERE customer_id = $1\n                        \"\"\",\n                        event.data['customer_id'],\n                        event.data['submitted_at']\n                    )\n\n                elif event.event_type == \"CustomerTierChanged\":\n                    await conn.execute(\n                        \"\"\"\n                        UPDATE customers SET tier = $2, updated_at = NOW()\n                        WHERE customer_id = $1\n                        \"\"\",\n                        event.data['customer_id'],\n                        event.data['new_tier']\n                    )\n```\n\n## Best Practices\n\n### Do's\n- **Make projections idempotent** - Safe to replay\n- **Use transactions** - For multi-table updates\n- **Store checkpoints** - Resume after failures\n- **Monitor lag** - Alert on projection delays\n- **Plan for rebuilds** - Design for reconstruction\n\n### Don'ts\n- **Don't couple projections** - Each is independent\n- **Don't skip error handling** - Log and alert on failures\n- **Don't ignore ordering** - Events must be processed in order\n- **Don't over-normalize** - Denormalize for query patterns\n\n## Resources\n\n- [CQRS Pattern](https:\u002F\u002Fdocs.microsoft.com\u002Fen-us\u002Fazure\u002Farchitecture\u002Fpatterns\u002Fcqrs)\n- [Projection Building Blocks](https:\u002F\u002Fzimarev.com\u002Fblog\u002Fevent-sourcing\u002Fprojections\u002F)\n","# Projection Patterns\n\nComprehensive guide to building projections and read models for event-sourced systems.\n\n## When to Use This Skill\n\n- Building CQRS read models\n- Creating materialized views from events\n- Optimizing query performance\n- Implementing real-time dashboards\n- Building search indexes from events\n- Aggregating data across streams\n\n## Core Concepts\n\n### 1. Projection Architecture\n\n```\n┌─────────────┐     ┌─────────────┐     ┌─────────────┐\n│ Event Store │────►│ Projector   │────►│ Read Model  │\n│             │     │             │     │ (Database)  │\n│ ┌─────────┐ │     │ ┌─────────┐ │     │ ┌─────────┐ │\n│ │ Events  │ │     │ │ Handler │ │     │ │ Tables  │ │\n│ └─────────┘ │     │ │ Logic   │ │     │ │ Views   │ │\n│             │     │ └─────────┘ │     │ │ Cache   │ │\n└─────────────┘     └─────────────┘     └─────────────┘\n```\n\n### 2. Projection Types\n\n| Type | Description | Use Case |\n|------|-------------|----------|\n| **Live** | Real-time from subscription | Current state queries |\n| **Catchup** | Process historical events | Rebuilding read models |\n| **Persistent** | Stores checkpoint | Resume after restart |\n| **Inline** | Same transaction as write | Strong consistency |\n\n## Templates\n\n### Template 1: Basic Projector\n\n```python\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass\nfrom typing import Dict, Any, Callable, List\nimport asyncpg\n\n@dataclass\nclass Event:\n    stream_id: str\n    event_type: str\n    data: dict\n    version: int\n    global_position: int\n\n\nclass Projection(ABC):\n    \"\"\"Base class for projections.\"\"\"\n\n    @property\n    @abstractmethod\n    def name(self) -> str:\n        \"\"\"Unique projection name for checkpointing.\"\"\"\n        pass\n\n    @abstractmethod\n    def handles(self) -> List[str]:\n        \"\"\"List of event types this projection handles.\"\"\"\n        pass\n\n    @abstractmethod\n    async def apply(self, event: Event) -> None:\n        \"\"\"Apply event to the read model.\"\"\"\n        pass\n\n\nclass Projector:\n    \"\"\"Runs projections from event store.\"\"\"\n\n    def __init__(self, event_store, checkpoint_store):\n        self.event_store = event_store\n        self.checkpoint_store = checkpoint_store\n        self.projections: List[Projection] = []\n\n    def register(self, projection: Projection):\n        self.projections.append(projection)\n\n    async def run(self, batch_size: int = 100):\n        \"\"\"Run all projections continuously.\"\"\"\n        while True:\n            for projection in self.projections:\n                await self._run_projection(projection, batch_size)\n            await asyncio.sleep(0.1)\n\n    async def _run_projection(self, projection: Projection, batch_size: int):\n        checkpoint = await self.checkpoint_store.get(projection.name)\n        position = checkpoint or 0\n\n        events = await self.event_store.read_all(position, batch_size)\n\n        for event in events:\n            if event.event_type in projection.handles():\n                await projection.apply(event)\n\n            await self.checkpoint_store.save(\n                projection.name,\n                event.global_position\n            )\n\n    async def rebuild(self, projection: Projection):\n        \"\"\"Rebuild a projection from scratch.\"\"\"\n        await self.checkpoint_store.delete(projection.name)\n        # Optionally clear read model tables\n        await self._run_projection(projection, batch_size=1000)\n```\n\n### Template 2: Order Summary Projection\n\n```python\nclass OrderSummaryProjection(Projection):\n    \"\"\"Projects order events to a summary read model.\"\"\"\n\n    def __init__(self, db_pool: asyncpg.Pool):\n        self.pool = db_pool\n\n    @property\n    def name(self) -> str:\n        return \"order_summary\"\n\n    def handles(self) -> List[str]:\n        return [\n            \"OrderCreated\",\n            \"OrderItemAdded\",\n            \"OrderItemRemoved\",\n            \"OrderShipped\",\n            \"OrderCompleted\",\n            \"OrderCancelled\"\n        ]\n\n    async def apply(self, event: Event) -> None:\n        handlers = {\n            \"OrderCreated\": self._handle_created,\n            \"OrderItemAdded\": self._handle_item_added,\n            \"OrderItemRemoved\": self._handle_item_removed,\n            \"OrderShipped\": self._handle_shipped,\n            \"OrderCompleted\": self._handle_completed,\n            \"OrderCancelled\": self._handle_cancelled,\n        }\n\n        handler = handlers.get(event.event_type)\n        if handler:\n            await handler(event)\n\n    async def _handle_created(self, event: Event):\n        async with self.pool.acquire() as conn:\n            await conn.execute(\n                \"\"\"\n                INSERT INTO order_summaries\n                (order_id, customer_id, status, total_amount, item_count, created_at)\n                VALUES ($1, $2, $3, $4, $5, $6)\n                \"\"\",\n                event.data['order_id'],\n                event.data['customer_id'],\n                'pending',\n                0,\n                0,\n                event.data['created_at']\n            )\n\n    async def _handle_item_added(self, event: Event):\n        async with self.pool.acquire() as conn:\n            await conn.execute(\n                \"\"\"\n                UPDATE order_summaries\n                SET total_amount = total_amount + $2,\n                    item_count = item_count + 1,\n                    updated_at = NOW()\n                WHERE order_id = $1\n                \"\"\",\n                event.data['order_id'],\n                event.data['price'] * event.data['quantity']\n            )\n\n    async def _handle_item_removed(self, event: Event):\n        async with self.pool.acquire() as conn:\n            await conn.execute(\n                \"\"\"\n                UPDATE order_summaries\n                SET total_amount = total_amount - $2,\n                    item_count = item_count - 1,\n                    updated_at = NOW()\n                WHERE order_id = $1\n                \"\"\",\n                event.data['order_id'],\n                event.data['price'] * event.data['quantity']\n            )\n\n    async def _handle_shipped(self, event: Event):\n        async with self.pool.acquire() as conn:\n            await conn.execute(\n                \"\"\"\n                UPDATE order_summaries\n                SET status = 'shipped',\n                    shipped_at = $2,\n                    updated_at = NOW()\n                WHERE order_id = $1\n                \"\"\",\n                event.data['order_id'],\n                event.data['shipped_at']\n            )\n\n    async def _handle_completed(self, event: Event):\n        async with self.pool.acquire() as conn:\n            await conn.execute(\n                \"\"\"\n                UPDATE order_summaries\n                SET status = 'completed',\n                    completed_at = $2,\n                    updated_at = NOW()\n                WHERE order_id = $1\n                \"\"\",\n                event.data['order_id'],\n                event.data['completed_at']\n            )\n\n    async def _handle_cancelled(self, event: Event):\n        async with self.pool.acquire() as conn:\n            await conn.execute(\n                \"\"\"\n                UPDATE order_summaries\n                SET status = 'cancelled',\n                    cancelled_at = $2,\n                    cancellation_reason = $3,\n                    updated_at = NOW()\n                WHERE order_id = $1\n                \"\"\",\n                event.data['order_id'],\n                event.data['cancelled_at'],\n                event.data.get('reason')\n            )\n```\n\n### Template 3: Elasticsearch Search Projection\n\n```python\nfrom elasticsearch import AsyncElasticsearch\n\nclass ProductSearchProjection(Projection):\n    \"\"\"Projects product events to Elasticsearch for full-text search.\"\"\"\n\n    def __init__(self, es_client: AsyncElasticsearch):\n        self.es = es_client\n        self.index = \"products\"\n\n    @property\n    def name(self) -> str:\n        return \"product_search\"\n\n    def handles(self) -> List[str]:\n        return [\n            \"ProductCreated\",\n            \"ProductUpdated\",\n            \"ProductPriceChanged\",\n            \"ProductDeleted\"\n        ]\n\n    async def apply(self, event: Event) -> None:\n        if event.event_type == \"ProductCreated\":\n            await self.es.index(\n                index=self.index,\n                id=event.data['product_id'],\n                document={\n                    'name': event.data['name'],\n                    'description': event.data['description'],\n                    'category': event.data['category'],\n                    'price': event.data['price'],\n                    'tags': event.data.get('tags', []),\n                    'created_at': event.data['created_at']\n                }\n            )\n\n        elif event.event_type == \"ProductUpdated\":\n            await self.es.update(\n                index=self.index,\n                id=event.data['product_id'],\n                doc={\n                    'name': event.data['name'],\n                    'description': event.data['description'],\n                    'category': event.data['category'],\n                    'tags': event.data.get('tags', []),\n                    'updated_at': event.data['updated_at']\n                }\n            )\n\n        elif event.event_type == \"ProductPriceChanged\":\n            await self.es.update(\n                index=self.index,\n                id=event.data['product_id'],\n                doc={\n                    'price': event.data['new_price'],\n                    'price_updated_at': event.data['changed_at']\n                }\n            )\n\n        elif event.event_type == \"ProductDeleted\":\n            await self.es.delete(\n                index=self.index,\n                id=event.data['product_id']\n            )\n```\n\n### Template 4: Aggregating Projection\n\n```python\nclass DailySalesProjection(Projection):\n    \"\"\"Aggregates sales data by day for reporting.\"\"\"\n\n    def __init__(self, db_pool: asyncpg.Pool):\n        self.pool = db_pool\n\n    @property\n    def name(self) -> str:\n        return \"daily_sales\"\n\n    def handles(self) -> List[str]:\n        return [\"OrderCompleted\", \"OrderRefunded\"]\n\n    async def apply(self, event: Event) -> None:\n        if event.event_type == \"OrderCompleted\":\n            await self._increment_sales(event)\n        elif event.event_type == \"OrderRefunded\":\n            await self._decrement_sales(event)\n\n    async def _increment_sales(self, event: Event):\n        date = event.data['completed_at'][:10]  # YYYY-MM-DD\n        async with self.pool.acquire() as conn:\n            await conn.execute(\n                \"\"\"\n                INSERT INTO daily_sales (date, total_orders, total_revenue, total_items)\n                VALUES ($1, 1, $2, $3)\n                ON CONFLICT (date) DO UPDATE SET\n                    total_orders = daily_sales.total_orders + 1,\n                    total_revenue = daily_sales.total_revenue + $2,\n                    total_items = daily_sales.total_items + $3,\n                    updated_at = NOW()\n                \"\"\",\n                date,\n                event.data['total_amount'],\n                event.data['item_count']\n            )\n\n    async def _decrement_sales(self, event: Event):\n        date = event.data['original_completed_at'][:10]\n        async with self.pool.acquire() as conn:\n            await conn.execute(\n                \"\"\"\n                UPDATE daily_sales SET\n                    total_orders = total_orders - 1,\n                    total_revenue = total_revenue - $2,\n                    total_refunds = total_refunds + $2,\n                    updated_at = NOW()\n                WHERE date = $1\n                \"\"\",\n                date,\n                event.data['refund_amount']\n            )\n```\n\n### Template 5: Multi-Table Projection\n\n```python\nclass CustomerActivityProjection(Projection):\n    \"\"\"Projects customer activity across multiple tables.\"\"\"\n\n    def __init__(self, db_pool: asyncpg.Pool):\n        self.pool = db_pool\n\n    @property\n    def name(self) -> str:\n        return \"customer_activity\"\n\n    def handles(self) -> List[str]:\n        return [\n            \"CustomerCreated\",\n            \"OrderCompleted\",\n            \"ReviewSubmitted\",\n            \"CustomerTierChanged\"\n        ]\n\n    async def apply(self, event: Event) -> None:\n        async with self.pool.acquire() as conn:\n            async with conn.transaction():\n                if event.event_type == \"CustomerCreated\":\n                    # Insert into customers table\n                    await conn.execute(\n                        \"\"\"\n                        INSERT INTO customers (customer_id, email, name, tier, created_at)\n                        VALUES ($1, $2, $3, 'bronze', $4)\n                        \"\"\",\n                        event.data['customer_id'],\n                        event.data['email'],\n                        event.data['name'],\n                        event.data['created_at']\n                    )\n                    # Initialize activity summary\n                    await conn.execute(\n                        \"\"\"\n                        INSERT INTO customer_activity_summary\n                        (customer_id, total_orders, total_spent, total_reviews)\n                        VALUES ($1, 0, 0, 0)\n                        \"\"\",\n                        event.data['customer_id']\n                    )\n\n                elif event.event_type == \"OrderCompleted\":\n                    # Update activity summary\n                    await conn.execute(\n                        \"\"\"\n                        UPDATE customer_activity_summary SET\n                            total_orders = total_orders + 1,\n                            total_spent = total_spent + $2,\n                            last_order_at = $3\n                        WHERE customer_id = $1\n                        \"\"\",\n                        event.data['customer_id'],\n                        event.data['total_amount'],\n                        event.data['completed_at']\n                    )\n                    # Insert into order history\n                    await conn.execute(\n                        \"\"\"\n                        INSERT INTO customer_order_history\n                        (customer_id, order_id, amount, completed_at)\n                        VALUES ($1, $2, $3, $4)\n                        \"\"\",\n                        event.data['customer_id'],\n                        event.data['order_id'],\n                        event.data['total_amount'],\n                        event.data['completed_at']\n                    )\n\n                elif event.event_type == \"ReviewSubmitted\":\n                    await conn.execute(\n                        \"\"\"\n                        UPDATE customer_activity_summary SET\n                            total_reviews = total_reviews + 1,\n                            last_review_at = $2\n                        WHERE customer_id = $1\n                        \"\"\",\n                        event.data['customer_id'],\n                        event.data['submitted_at']\n                    )\n\n                elif event.event_type == \"CustomerTierChanged\":\n                    await conn.execute(\n                        \"\"\"\n                        UPDATE customers SET tier = $2, updated_at = NOW()\n                        WHERE customer_id = $1\n                        \"\"\",\n                        event.data['customer_id'],\n                        event.data['new_tier']\n                    )\n```\n\n## Best Practices\n\n### Do's\n- **Make projections idempotent** - Safe to replay\n- **Use transactions** - For multi-table updates\n- **Store checkpoints** - Resume after failures\n- **Monitor lag** - Alert on projection delays\n- **Plan for rebuilds** - Design for reconstruction\n\n### Don'ts\n- **Don't couple projections** - Each is independent\n- **Don't skip error handling** - Log and alert on failures\n- **Don't ignore ordering** - Events must be processed in order\n- **Don't over-normalize** - Denormalize for query patterns\n\n## Resources\n\n- [CQRS Pattern](https:\u002F\u002Fdocs.microsoft.com\u002Fen-us\u002Fazure\u002Farchitecture\u002Fpatterns\u002Fcqrs)\n- [Projection Building Blocks](https:\u002F\u002Fzimarev.com\u002Fblog\u002Fevent-sourcing\u002Fprojections\u002F)\n",{"name":5,"description":7},"main","7850740bd837adb32f0a109ab2af82482196c6f2","skills\u002F_collections\u002Fplugins\u002Fbackend-development\u002Fskills\u002Fprojection-patterns",1,"https:\u002F\u002Fgithub.com\u002Fshengdabai\u002FTony-Claude-Code-Skills","https:\u002F\u002Fzturnsgo.com","https:\u002F\u002Fgithub.com\u002Fshengdabai","https:\u002F\u002Fzturnsgo.com\u002F","AI × education creator — producer, not consumer. Taught 6,000+ learners; shipping learning tools & daily essays with Claude Code, practicing @xiaolai's way 🥋","2026-11-01 21:28:23",{"source":8,"skillName":5,"commands":50,"prompt":60,"zipAvailable":61},[51,54,57],{"tool":52,"command":53},"npx","npx skills add shengdabai\u002FTony-Claude-Code-Skills --skill projection-patterns",{"tool":55,"command":56},"bunx","bunx skills add shengdabai\u002FTony-Claude-Code-Skills --skill projection-patterns",{"tool":58,"command":59},"pnpm","pnpm dlx skills add shengdabai\u002FTony-Claude-Code-Skills --skill projection-patterns","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fshengdabai\u002FTony-Claude-Code-Skills\u002Fblob\u002Fmain\u002Fskills\u002F_collections\u002Fplugins\u002Fbackend-development\u002Fskills\u002Fprojection-patterns\u002FSKILL.md",true,[],[64,83,117,142,164,184],{"skillNo":65,"name":66,"nameZh":6,"description":67,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":68,"categories":69,"keywords":75,"publishedAt":34},"01M70P17DAD4C1C47NPH6A96EM","xiaolai-methodology","以李笑来(Li Xiaolai)的思考方式、方法论与表达风格,为用户做全方位 1-on-1 教练指导,覆盖创业、赚钱、投资、学习、自学、读书写作、用 AI、以及人生心态与自我管理。当用户提到 \"李笑来\"、\"笑来\"、\"笑来老师\"、\"扮演李笑来\"、\"找笑来聊聊\"、\"笑来怎么看\"、\"xiaolai\"、\"把时间当作朋友\"、\"通往财富自由之路\"、\"定投改变命运\"、\"韭菜的自我修养\"、\"自学是门手艺\"、\"让时间陪你慢慢变富\"、\"七年就是一辈子\"、\"元认知\"、\"活在未来\"、\"复利\u002F长期主义\"、\"践行\"、\"痴迷改进\"、\"个人商业模式\u002F出售时间\"、\"睡后收入\"、\"定投\"、\"财富自由\"、\"操作系统升级\"、\"最少必要知识\"、\"北极星提示词\"、\"用 AI 该怎么学\u002F思考\" 等任一触发词,或当用户带着关于创业\u002F赚钱\u002F投资\u002F学习\u002F职业\u002F重大人生决策的困惑、希望被人用笑来式的「逻辑 + 大白话 + 类比 + 重新定义 + 践行导向」来点醒和指导时,自动进入李笑来教练模式。","https:\u002F\u002Fgithub.com\u002Fshengdabai\u002FTony-Claude-Code-Skills\u002Fblob\u002Fmain\u002Fxiaolai-methodology\u002FSKILL.md",[70],{"categoryNo":71,"categoryName":72,"taxonomyType":73,"taxonomyTypeDesc":74},"01JBSEEDCAT000000000000018","写作与文案","SCENARIO","使用场景",[76,77,78,79,80,81,82],"methodology","xiaolai","xiaolai methodology","朋友","投资","writing","收入",{"skillNo":84,"name":85,"nameZh":6,"description":86,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":87,"categories":88,"keywords":97,"publishedAt":34},"01M70P13NB9YP7ACJR1S861CDS","zread","Produce and consume a wiki-style knowledge base for a code repository via the `zread` CLI and its on-disk output under `.\u002F.zread\u002Fwiki\u002F`. Use this skill whenever the user wants to understand, onboard onto, explore, summarize, map, or get an overview of an unfamiliar codebase; asks for architecture docs, a project wiki, a repo walkthrough, module\u002Fpackage explanations, or \"what does this repo do\"; wants to generate, regenerate, resume, browse, or serve code documentation locally; or mentions zread \u002F zread.ai directly. Also use it proactively before diving into a large unknown repo — if `.\u002F.zread\u002Fwiki\u002Fcurrent` exists, read the generated pages instead of crawling source file-by-file; if it doesn't, consider offering to run `zread generate`. The trigger is the intent (understand a codebase through generated docs), not the literal word \"zread\".","https:\u002F\u002Fgithub.com\u002Fshengdabai\u002FTony-Claude-Code-Skills\u002Fblob\u002Fmain\u002Fskills\u002Fzread\u002FSKILL.md",[89,92],{"categoryNo":90,"categoryName":91,"taxonomyType":73,"taxonomyTypeDesc":74},"01JBSEEDCAT000000000000023","Skill与智能体开发",{"categoryNo":93,"categoryName":94,"taxonomyType":95,"taxonomyTypeDesc":96},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[85,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116],"knowledge","wiki","explore","walkthrough","project","get","knowledge base","run","generate","intent","cli","map","documentation","project wiki","disk","architecture","code","unknown","generated",{"skillNo":118,"name":119,"nameZh":6,"description":120,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":121,"categories":122,"keywords":127,"publishedAt":34},"01M70P10S4SWMZQ8BJAFBFDRW3","youtube","Launch the YouTube Creator Agent to auto-generate videos.\nUse when: user types \u002Fyoutube, wants to create YouTube videos,\ngenerate video content, or upload to YouTube.\nKeywords: youtube, video, 视频, 创作视频, 发布视频","https:\u002F\u002Fgithub.com\u002Fshengdabai\u002FTony-Claude-Code-Skills\u002Fblob\u002Fmain\u002Fskills\u002Fyoutube\u002FSKILL.md",[123,124],{"categoryNo":90,"categoryName":91,"taxonomyType":73,"taxonomyTypeDesc":74},{"categoryNo":125,"categoryName":126,"taxonomyType":95,"taxonomyTypeDesc":96},"01JBSEEDCAT000000000000036","内容创作者",[119,128,129,130,131,132,133,134,106,135,136,137,27,26,138,139,140,141],"user","content","视频","creator","generate video","videos","launch","video content","create","upload","auto","types","Video","创作",{"skillNo":143,"name":144,"nameZh":6,"description":145,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":146,"categories":147,"keywords":149,"publishedAt":34},"01M70P0YE95D8BACZCGV8BK90K","youtube-script-sync","Sync, update, or optimize Tony's GetNote knowledge base `Youtube视频逐字稿` by turning each parent recording or first-person idea note into an idempotent child note containing a directly recordable YouTube long-video or Shorts script. Use when the user says “更新\u002F同步\u002F优化 Youtube视频逐字稿知识库”, or says “更新\u002F同步\u002F优化该知识库” while this knowledge base is the active context.","https:\u002F\u002Fgithub.com\u002Fshengdabai\u002FTony-Claude-Code-Skills\u002Fblob\u002Fmain\u002Fskills\u002Fyoutube-script-sync\u002FSKILL.md",[148],{"categoryNo":90,"categoryName":91,"taxonomyType":73,"taxonomyTypeDesc":74},[150,151,119,152,153,140,154,155,156,157,158,159,160,161,104,162,27,26,98,163],"sync","script","youtube script sync","recording","shorts","first","base","person","update","active","child","optimize","idempotent","long",{"skillNo":165,"name":166,"nameZh":6,"description":167,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":168,"categories":169,"keywords":174,"publishedAt":34},"01M70P0WMV5XT0EEC0N445PW07","writing-skills","How to write SKILL.md files that trigger reliably and teach effectively. Use when creating, improving, or reviewing Claude Code skills.","https:\u002F\u002Fgithub.com\u002Fshengdabai\u002FTony-Claude-Code-Skills\u002Fblob\u002Fmain\u002Fskills\u002Fxiaolai\u002Fnlpm\u002Fskills\u002Fnlpm\u002Fwriting-skills\u002FSKILL.md",[170,171,172,173],{"categoryNo":71,"categoryName":72,"taxonomyType":73,"taxonomyTypeDesc":74},{"categoryNo":90,"categoryName":91,"taxonomyType":73,"taxonomyTypeDesc":74},{"categoryNo":93,"categoryName":94,"taxonomyType":95,"taxonomyTypeDesc":96},{"categoryNo":125,"categoryName":126,"taxonomyType":95,"taxonomyTypeDesc":96},[175,81,176,177,178,179,114,27,26,180,181,182,183],"writing skills","how","teach","write skill","write","claude code skills","files","how to","trigger",{"skillNo":185,"name":186,"nameZh":6,"description":187,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":188,"categories":189,"keywords":192,"publishedAt":34},"01M70P0SJFR4YVJQBGMHKGSKXT","writing-rules","How to write .claude\u002Frules\u002F files that Claude actually follows. Use when creating, improving, or reviewing project rules.","https:\u002F\u002Fgithub.com\u002Fshengdabai\u002FTony-Claude-Code-Skills\u002Fblob\u002Fmain\u002Fskills\u002Fxiaolai\u002Fnlpm\u002Fskills\u002Fnlpm\u002Fwriting-rules\u002FSKILL.md",[190,191],{"categoryNo":71,"categoryName":72,"taxonomyType":73,"taxonomyTypeDesc":74},{"categoryNo":125,"categoryName":126,"taxonomyType":95,"taxonomyTypeDesc":96},[193,81,194,102,195,181,27,176,179,26,182],"Rules","writing rules","project rules"]