提示:本网站现在为维护调试状态

CQRS + Event Sourcing アーキテクチャ解説

Choyeon
Choyeon
2026年9月14日 1 分で読了 382 閲覧
545 字 3,229 文字 更新日 2026年11月8日
CQRS + Event Sourcing アーキテクチャ解説
目次

CQRS + Event Sourcing Architecture Patterns

As systems scale, traditional CRUD single-model architectures hit bottlenecks in complex queries, audit trails, and concurrency. CQRS + ES are advanced patterns that break these limits, but added complexity demands careful adoption decisions.

Event Sourcing: Events as Source of Truth

Never persist current state; append only events that caused state changes (OrderCreated → ItemAdded → OrderPlaced). Replaying reconstructs state at any point; complete audit trail for free. Pair with snapshots to avoid replaying from zero on each load.

from __future__ import annotations
import json, time
from dataclasses import dataclass, field
from typing import Callable, Iterable, Any
from collections import defaultdict

@dataclass
class DomainEvent:
    event_id: str
    aggregate_type: str
    aggregate_id: str
    event_type: str
    payload: dict
    occurred_at: float = field(default_factory=time.time)
    version: int = 1

class EventStore:
    def __init__(self) -> None:
        self._events: list[DomainEvent] = []
        self._snapshots: dict[tuple[str, str], tuple[int, Any]] = {}
        self._subs: dict[str, list[Callable[[DomainEvent], None]]] = defaultdict(list)

    def append(self, events: Iterable[DomainEvent], expected_version: int | None = None) -> None:
        events = list(events)
        if expected_version is not None and events:
            at, aid = events[0].aggregate_type, events[0].aggregate_id
            cur = sum(1 for e in self._events if e.aggregate_type == at and e.aggregate_id == aid)
            if cur != expected_version:
                raise ValueError(f"Concurrency conflict: expected v{expected_version}, got v{cur}")
        for e in events:
            self._events.append(e)
            for s in self._subs.get(e.event_type, []) + self._subs.get("*", []): s(e)

    def load(self, agg_type: str, agg_id: str, snapshot_every: int = 50) -> tuple[Any, int]:
        key = (agg_type, agg_id)
        start_ver, state = self._snapshots.get(key, (0, None))
        version = start_ver
        events = [e for e in self._events if e.aggregate_type == agg_type and e.aggregate_id == agg_id][start_ver:]
        for e in events:
            version += 1
            fn = globals().get(f"apply_{e.event_type}")
            state = fn(state, e) if fn else state
            if version % snapshot_every == 0: self._snapshots[key] = (version, state)
        return state, version

    def subscribe(self, event_type: str, fn: Callable[[DomainEvent], None]) -> None:
        self._subs[event_type].append(fn)

def apply_OrderCreated(state, e): return {"order_id": e.aggregate_id, "items": {}, "status": "CREATED", **e.payload}
def apply_ItemAdded(state, e):
    s = dict(state); pid = e.payload["product_id"]
    s["items"] = {**s["items"], pid: s["items"].get(pid, 0) + e.payload["qty"]}
    return s
def apply_OrderPlaced(state, e): return {**state, "status": "PLACED", "placed_at": e.occurred_at}

store = EventStore()
projection: dict[str, dict] = {}
store.subscribe("OrderPlaced", lambda e: projection.__setitem__(e.aggregate_id, {"status": "PLACED", **e.payload}))

CQRS: Separate Write and Read Models

Write path goes through domain model and produces events. Event subscribers asynchronously build denormalized projection tables tuned for queries. Write side enforces consistency while read side optimized for retrieval, but introduces an eventual consistency window.

Dimension Classic CRUD CQRS Only CQRS + Event Sourcing
Storage Single table both Write DB + Read DB Event stream + N projections
Query flexibility Medium Very High Very High + time travel
Audit history Extra columns Hard Free and immutable
Debug difficulty Low Medium-High Very High
Fit for 90% of business Read-heavy complex queries Finance/ledger/provenance

Best Practices

Ask three questions before adopting: Do you require an immutable audit trail? Are queries genuinely painful with CRUD? Does the team have DDD experience? Only proceed if all three are Yes. Otherwise classic CRUD plus a simple audit table nearly always delivers better ROI.

X微博Telegram

筆者について

投票

你最希望 Rosetta 优先加入哪项功能?

社区投票(演示数据,可后台删除)

コメント (6)

0 / 3000
东京留学僧
东京留学僧2026年9月12日

楼主,我按照你的步骤操作后启动报错:Error creating bean with name 'dataSource',版本是 Spring Boot 3.2,请问有遇到过吗?

摸鱼达人小王
摸鱼达人小王2026年9月12日

补充一点:实际使用中如果遇到 MySQL 的 8 小时超时问题,可以在连接串里加 autoReconnect=true 参数,虽然官方不推荐但挺好用的。

全栈打工人
全栈打工人2026年9月11日

代码示例里那个 SQL 查询是不是应该加个 LIMIT?不然数据量大的时候全表扫描会很慢哦。

机器学习萌新
机器学习萌新2026年9月11日

写得太好了,终于搞懂了分布式事务的几种方案区别,收藏了!

技术笔记控
技术笔记控tech-notes.fun2026年9月11日

作为一个刚入行的后端新人,这篇文章帮我理清了好多概念,谢谢博主持续输出高质量内容!

云端漫步
云端漫步cloud-notes.cloud2026年9月11日

太及时了,正好项目里要上 Redis 集群,照着来一遍省了好多事,谢谢!

関連記事

GraphQL vs REST:7つの視点から深く比較

GraphQL vs REST:7つの視点から深く比較

APIスタイル選択迷宮を終わらせる:ネットワーク効率・DX・進化コスト・キャッシュ・型安全性・セキュリティ・エコシステムの7次元で比較。明確なデシジョンツリーとハイブリッド構成を提示。

1 分で読了
JWT 認証セキュリティ + OAuth2 vs SAML

JWT 認証セキュリティ + OAuth2 vs SAML

JWT脆弱性と対策:alg混乱攻撃、鍵総当たり、KIDインジェクション、リプレイ対策。さらにOAuth2+PKCE、OIDC、SAML2.0の3プロトコル多次元比較。

1 分で読了
Prompt Engineering 入門:Few-Shot + CoT + 構造化出力

Prompt Engineering 入門:Few-Shot + CoT + 構造化出力

Prompt Engineering体系入門:3つのコアテクニック——Few-Shot例示学習でドメインスタイル注入、Chain-of-Thoughtで複雑推論誘導、JSON Schema強制構造化出力で下流システム連携。実テンプレートとアンチパターン集付き。

1 分で読了
Redis キャッシュ設計パターン

Redis キャッシュ設計パターン

3種のキャッシュパターン:Cache-Aside、Write-Through、Write-Behindの実装と適用シーン、キャッシュ問題対策まで。

1 分で読了
PostgreSQL クエリ最適化テクニック

PostgreSQL クエリ最適化テクニック

PostgreSQLクエリ最適化:EXPLAIN ANALYZEの読み方、インデックス種別選択、統計情報更新、結合戦略調整、パーティションテーブル設計。

1 分で読了
Alembic ディープガイド:DBマイグレーション + 自動生成 + ブランチマージ

Alembic ディープガイド:DBマイグレーション + 自動生成 + ブランチマージ

SQLAlchemy公式マイグレーションツール Alembic 完全ガイド:env.py設定、autogenerateによるモデル変更検出(カラム・インデックス・制約)、バッチスクリプト、ダウングレードロールバック戦略、複数人開発時のマイグレーションコンフリクトとhead分岐マージの標準SOP、本番デプロイチェックリスト付き。

1 分で読了