目次を表示する

DCB(Dynamic Consistency Boundary)

DCB の仕組み ── タグ・クエリ・楽観的ロック

DCB の仕組み ── タグ・クエリ・楽観的ロック


DCBの仕組み — タグ・クエリ・条件付き書き込み・競合検出

イベントの構造:タグ付きイベント

DCB のイベントストアでは、イベントはグローバルに順序付けられた1本のストリームに格納される。集約ごとのストリーム分割は行わない。

interface DcbEvent {
  // グローバル位置(単調増加)
  position: number;

  // イベントタイプ
  type: string;

  // タグ(複数可)
  tags: string[];

  // イベントデータ
  data: Record<string, unknown>;

  // メタデータ
  metadata: {
    timestamp: string;
    causation_id?: string;
    correlation_id?: string;
  };
}
実際のイベント例(講座予約):

Position 1: { type: "CourseCreated",    tags: ["course:C1"],               data: { name: "DDD入門", capacity: 30 } }
Position 2: { type: "StudentCreated",   tags: ["student:S1"],              data: { name: "田中太郎" } }
Position 3: { type: "StudentRegistered", tags: ["course:C1", "student:S1"], data: { course_id: "C1", student_id: "S1" } }
Position 4: { type: "StudentRegistered", tags: ["course:C1", "student:S2"], data: { course_id: "C1", student_id: "S2" } }
Position 5: { type: "StudentCreated",   tags: ["student:S3"],              data: { name: "鈴木花子" } }
Position 6: { type: "StudentRegistered", tags: ["course:C2", "student:S1"], data: { course_id: "C2", student_id: "S1" } }

イベントの読み取り:動的クエリ

「講座 C1 の登録者数」を知りたい場合

クエリ:
  type = "StudentRegistered" AND tags CONTAINS "course:C1"

結果:
  Position 3: { student_id: "S1" }
  Position 4: { student_id: "S2" }
  → 2名が登録済み(最終位置: 4)

「受講者 S1 の登録講座数」を知りたい場合

クエリ:
  type = "StudentRegistered" AND tags CONTAINS "student:S1"

結果:
  Position 3: { course_id: "C1" }
  Position 6: { course_id: "C2" }
  → 2講座に登録済み(最終位置: 6)

「受講者 S3 が講座 C1 に登録できるか」を判断する場合

2つのクエリを組み合わせる:

# Decision Model の構築
def build_registration_decision(event_store, student_id: str, course_id: str):
    # クエリ1: 講座の登録者数
    course_events = event_store.query(
        event_type="StudentRegistered",
        tags=[f"course:{course_id}"]
    )
    course_count = len(course_events)
    course_last_position = course_events[-1].position if course_events else 0

    # クエリ2: 受講者の登録講座数
    student_events = event_store.query(
        event_type="StudentRegistered",
        tags=[f"student:{student_id}"]
    )
    student_count = len(student_events)
    student_last_position = student_events[-1].position if student_events else 0

    return {
        "course_count": course_count,
        "student_count": student_count,
        "last_known_position": max(course_last_position, student_last_position),
        "queries": [
            {"type": "StudentRegistered", "tags": [f"course:{course_id}"]},
            {"type": "StudentRegistered", "tags": [f"student:{student_id}"]},
        ]
    }

一貫性の保証:条件付き書き込み

DCB の楽観的ロックは、集約のストリームバージョンではなく、クエリ結果に基づく前提条件で行われる。

def register_student(event_store, student_id: str, course_id: str):
    # 1. Decision Model を構築
    decision = build_registration_decision(event_store, student_id, course_id)

    # 2. ビジネスルールを検証
    if decision["course_count"] >= 30:
        raise DomainError("講座が満員です")
    if decision["student_count"] >= 10:
        raise DomainError("受講上限に達しています")

    # 3. 条件付き書き込み
    new_event = DcbEvent(
        type="StudentRegistered",
        tags=[f"course:{course_id}", f"student:{student_id}"],
        data={"course_id": course_id, "student_id": student_id}
    )

    try:
        event_store.append(
            event=new_event,
            preconditions=[
                # 「私がクエリした後に、同じ条件のイベントが追加されていないこと」
                {
                    "query": {"type": "StudentRegistered", "tags": [f"course:{course_id}"]},
                    "last_known_position": decision["last_known_position"]
                },
                {
                    "query": {"type": "StudentRegistered", "tags": [f"student:{student_id}"]},
                    "last_known_position": decision["last_known_position"]
                },
            ]
        )
    except PreconditionFailedError:
        # 他のリクエストが先に書き込んだ → リトライ
        return register_student(event_store, student_id, course_id)

競合の検出メカニズム

シナリオ: 2つのリクエストがほぼ同時に到着

Request A: 受講者 S3 を講座 C1 に登録(現在 29名)
Request B: 受講者 S4 を講座 C1 に登録(現在 29名)

Request A のフロー:
  1. クエリ → course:C1 の登録数 = 29(最終位置 P99)
  2. 29 < 30 → OK
  3. 条件付き書き込み → P99 以降に course:C1 の StudentRegistered はない → 成功
  4. 新イベント書き込み(Position P100)

Request B のフロー:
  1. クエリ → course:C1 の登録数 = 29(最終位置 P99)
  2. 29 < 30 → OK
  3. 条件付き書き込み → P99 以降に course:C1 の StudentRegistered がある(P100)
  4. → PreconditionFailedError!
  5. リトライ → 再クエリ → 登録数 = 30 → 「講座が満員です」

→ 30名の上限が確実に守られる

集約の楽観的ロック vs DCB の楽観的ロック

集約ベース:
  ストリーム "course-C1" のバージョン = 29
  → 書き込み時に「バージョンが 29 であること」を前提条件にする
  → course-C1 への全てのイベント(登録以外も含む)が競合対象

DCB ベース:
  「type=StudentRegistered AND tags=course:C1」のクエリ結果の最終位置
  → 書き込み時に「このクエリの最終位置が変わっていないこと」を前提条件にする
  → 登録に関係するイベントだけが競合対象
  → 講座名の変更や説明文の更新は競合しない

DCB の方が競合が少ない。集約ベースでは同じストリーム内の全操作がシリアライズされるが、DCB では同じ判断に影響するイベントだけが競合する。


書き込みの並行性

集約ベース:
  course-C1 ストリームへの書き込みは全てシリアル
  → 登録、講座名変更、説明文更新が全て直列化

DCB ベース:
  「登録」と「講座名変更」は異なるタグ/タイプ → 並行して書き込み可能
  「登録」同士のみが前提条件で競合する

→ 不要なシリアライゼーションが排除される

まとめ

DCB の構成要素役割
タグ付きイベント1つのイベントを複数の次元でスライス可能にする
動的クエリ判断に必要なイベントだけを実行時に選択する
Decision Modelクエリ結果からビジネスルールを検証する
条件付き書き込みクエリ後にイベントが追加されていないことを保証する
リトライ前提条件が失敗したらクエリからやり直す

次章では、これらの仕組みが実際のユースケースでどう活きるかを具体的に見ていく。