プレイブック

内側からの垂直スライスはどのように機能しますか? (Vertical Slice Inside Out)

垂直スライスは、リクエストから検証、ハンドラーから集計、アウトボックス イベントからモデルの読み取り、そしてクラウド コストまで、内部的にどのように流れるのでしょうか? CQRS…

垂直スライス — 機能指向エンジニアリング

一部 2 の 4

A vertical slice request pipeline from endpoint to handler, validation, persistence, event publication, and tests

30 秒で要約

HTTP リクエストがシステムに届くとき、実際には 1 つのメソッドに送られるわけではありません。まず、外部境界からインポートされ、検証され、認可制御を通過し、必要に応じてトランザクションが開かれ、ドメイン ルールが適用され、データが記録され、イベントが保護され、読み取り側が更新されます。

垂直スライスは、単一のユーザーの意図を中心にこのジャーニーを整理します。```text HTTP Request | v Endpoint / Controller | v Mediator.Send(command) | v Pipeline Behaviors | +--> Validation +--> Authorization +--> Logging / Metrics +--> Idempotency +--> Transaction v Command Handler | v Aggregate | v Repository | v Outbox | v Database Commit


## 問題: 小さな納品書はなぜ高価になるのでしょうか?

マーケットプレイス アプリケーションで注文に納品書を追加すると想像してみましょう。一見すると、これは単なる文字列フィールドです。ただし、運用環境では、同じフィールドが支払い概要、販売者パネル、配送統合、顧客の電子メール、および監査記録に表示されます。```text
AddDeliveryNote
  -> sipariş kuralı
  -> validasyon
  -> audit
  -> notification
  -> seller projection
  -> müşteri görünümü
```階層構造では、この変更は技術的役割全体に分散されます。垂直スライスでは、質問は変わります。この動作はどのユーザーの意図に属し、どの一貫性の制限で完了する必要がありますか?

## 最初に説明した概念```text
Command
Sistemin durumunu değiştirmek isteyen niyettir. Örneğin AddDeliveryNote bir command'dir; başarılı olabilir, reddedilebilir veya hata alabilir.

Query
Sistemin durumunu değiştirmeden veri okuma isteğidir. Örneğin GetOrderSummary bir query'dir.

Handler
Bir command veya query'nin uygulama seviyesindeki akışını yöneten sınıftır. Handler orkestrasyon yapar; iş kuralının kalıcı sahibi olmamalıdır.

Pipeline Behavior
Handler'a ulaşmadan önce veya sonra çalışan ortak teknik adımdır. Validation, authorization, logging, idempotency ve transaction burada konumlanabilir.

Aggregate
Sistemde asla bozulmaması gereken iş kurallarını, yani invariant'ları, koruyan domain nesnesidir. Örneğin iptal edilmiş siparişe teslimat notu eklenememesi aggregate kuralıdır.

Read Model
Domain entity'nin kopyası değildir; bir ekranın veya API cevabının ihtiyaç duyduğu okuma şeklidir.

Projection
Event veya write model değişikliklerinden read model üreten süreçtir. Örneğin DeliveryNoteUpdated event'inden SellerOrderSummary read model'ini günceller.

Outbox
Veritabanı değişikliği ile event yayınını aynı local transaction içinde güvenceye alan kayıt desenidir.

CDC
Change Data Capture, veritabanı loglarından değişiklikleri okuyup dış sistemlere taşıma yaklaşımıdır. Domain event tasarımı yerine DB değişimini kaynak alır.
```これらの部品は交換できません。ハンドラーはドメイン ルールではありません。ドメインの動作を呼び出します。パイプラインはビジネス ポリシーではありません。一般的な用途はセキュリティラインです。 Outbox はブローカーではありません。失われるべきではないイベントのトランザクション記録です。

## 実話: 注文メモのスライス

`AddDeliveryNote` のユースケースを設計してみましょう。ユーザーは注文に納品書を追加したいと考えています。システムはまずユーザーの順序変更の権利をチェックし、音符の長さを確認し、集計上で動作を実行し、変更を保存し、他の画面を更新するイベントを生成します。```text
Features/Orders/AddDeliveryNote
  AddDeliveryNoteEndpoint.cs
  AddDeliveryNoteCommand.cs
  AddDeliveryNoteValidator.cs
  AddDeliveryNoteHandler.cs
  AddDeliveryNoteResponse.cs
  AddDeliveryNoteTests.cs
```このディレクトリは「ミニ レイヤー」の墓場であってはなりません。各ファイルには 1 つの責任があります。エンドポイントは HTTP コントラクトを伝達し、コマンドは意図を表現し、バリデータは入力制限を維持し、ハンドラーはフローを管理し、集計はビジネス ルールを実装し、テストは動作を証明します。

## アルゴリズム設計

擬似コードレベルでは、スライスは次のように動作します。```text
algorithm AddDeliveryNote(command):
  input: orderId, customerId, note, requestId
  output: AddDeliveryNoteResult

  validate command
  reject if requestId was processed before

  order = orderRepository.load(orderId)
  reject if order does not exist
  reject if order.customerId != customerId

  order.addDeliveryNote(note)
  outbox.add(DeliveryNoteUpdated(orderId, note, occurredAt))

  transaction.commit()
  return success(orderId, note)
```通常のフローでは時間計算量は O(1) です。単一の集約 ID を介してロードされるため、固定数のルールが実行され、固定数のイベントが書き込まれます。一意のインデックスを使用して冪等性制御が行われる場合、検索コストは実質的に O(log n) ですが、ハッシュ ベースのキャッシュまたはキー/値ストアの場合、平均コストは O(1) です。メモリの複雑さは O(1) です。ハンドラーは、注文履歴全体ではなく、必要な集約ステータスとコマンド データのみを伝送します。

## なぜ問題が発生したのでしょうか?

通常、問題はコードの長さではありません。問題は、同じ動作が複数の技術レイヤーにわたって半分の情報で表現されていることです。```text
Controller doğrular gibi yapar
Service iş kuralı gibi yapar
Repository veri kuralı gibi yapar
Frontend tekrar kontrol eder
Test mock sırasını korur
```このモデルでは、単一責任は表示されますが、行動レベルでは表示されません。各クラスは技術的には小さくすることができます。しかし、ビジネス上の決定は細分化されています。垂直スライスは、SOLID をクラス名から動作の所有権に移動します。ここでのインターフェイス分離とは、`IOrderService` のような肥大化したコントラクトの代わりに、`IAddDeliveryNoteAuthorization`、`IOrderRepository`、`IRequestDeduplicationStore` などの狭いポートを使用することを意味します。

## CQRS および Mediator パイプライン

コマンドとクエリの区別により、スライスの意図が明確になります。```csharp
public sealed record AddDeliveryNoteCommand(
    Guid OrderId,
    Guid CustomerId,
    string Note,
    string RequestId) : ICommand<AddDeliveryNoteResult>;

public sealed class AddDeliveryNoteHandler(
    IOrderRepository orders,
    IRequestDeduplicationStore deduplication,
    IOutboxWriter outbox,
    IUnitOfWork unitOfWork)
    : ICommandHandler<AddDeliveryNoteCommand, AddDeliveryNoteResult>
{
    public async Task<AddDeliveryNoteResult> Handle(
        AddDeliveryNoteCommand command,
        CancellationToken cancellationToken)
    {
        if (await deduplication.ExistsAsync(command.RequestId, cancellationToken))
        {
            return AddDeliveryNoteResult.AlreadyProcessed(command.OrderId);
        }

        var order = await orders.GetByIdAsync(command.OrderId, cancellationToken)
            ?? throw new OrderNotFoundException(command.OrderId);

        order.EnsureOwnedBy(command.CustomerId);
        order.AddDeliveryNote(command.Note);

        await outbox.AddAsync(
            DeliveryNoteUpdated.From(order),
            cancellationToken);

        await unitOfWork.CommitAsync(cancellationToken);
        return AddDeliveryNoteResult.Updated(order.Id, order.DeliveryNote);
    }
}
```ここでの Mediator の使用はツールであり、目的ではありません。本当の価値は、パイプラインが検証、認可、トランザクション、ロギング、冪等性などの反復的なフローをハンドラーの外に移動させることです。ただし、ライブラリの選択は、技術的パフォーマンスだけでなく、ライセンス、AOT 準拠、反映コスト、機関の承認プロセスも考慮して評価する必要があります。フレームワークの決定を恒久的なものにする前に、ライセンス テキストと組織のポリシーを確認する必要があります。

## クエリ側の流れはどのようになりますか?

コマンド側はビジネス ルールとトランザクションに重点を置いています。クエリ側は、読み取り効率とユーザー エクスペリエンスに焦点を当てます。ほとんどの場合、注文のリストを表示するために集計をロードする必要はありません。画面に適したナローリードモデルで十分です。```text
GET /orders
      |
      v
Authorization
      |
      v
Cache
      |
      v
Read Database / Search Index
      |
      v
DTO Mapping
      |
      v
Response
```キャッシュは、このチェーンにおける単なるパフォーマンス ツールではありません。それはコストの判断でもあります。頻繁に読み取られ、ほとんど変更されず、数秒の遅延が許容されるデータはキャッシュに適しています。支払いステータスなど、強い一貫性が必要なデータの場合は、キャッシュ ポリシーをより慎重に設計する必要があります。

## 代替案

|代替案 |強さ |承認された価格 |
| --- | --- | --- |
|シンプルなCRUDサービス |最低の初期費用 |行動が拡大するにつれて、共通のサービスも拡大します。
|垂直スライス、CQRS なし |地域性を保証する機能 |読み取り/書き込みの意図が見えにくくなる可能性があります。
|垂直スライス + CQRS |インテント、テスト、パイプラインの区別が明確になります |より多くの教室と契約の規律が必要 |
|物理的に分離された読み取り/書き込みモデル |読み取りパフォーマンスとスケールの独立性 |結果整合性、送信トレイ、投影、運用コストを追加 |
|マイクロサービス |独立した導入と拡張 |ネットワーク、データの一貫性、可観測性、チームのコストが増加 |

小さな CRUD サーフェスでは、多くの場合、垂直スライス + 単純なハンドラーが最善の決定となります。物理 CQRS またはマイクロサービスは、読み取り/書き込みのオーバーヘッド、チームの所有権、および障害の分離がコストに見合った場合にのみ選択する必要があります。

## 決定: 最初に論理的な境界、次に物理的な分離

私のデフォルトの決定は次のとおりです。まずアプリケーション内のスライスを論理的に分離します。次に、測定により物理的な分離が必要な場合は、読み取りモデル、ブローカー、キャッシュ、または別個のサービスを追加します。```text
Aşama 1: Modular monolith içinde slice
Aşama 2: Command/query ayrımı
Aşama 3: Outbox ve projection
Aşama 4: Ayrı read store veya cache
Aşama 5: Gerekirse ayrı deploy birimi
```この順序によりコストは延期されますが、設計は延期されません。コード境界が正しく描画されている場合、物理的な分離は書き換えではなく、制御された減算によって行われます。

## データ分離: 論理的と物理的は同じではありません

リードレプリカは CQRS ではありません。レプリカは別のノードから同じスキーマを読み取ります。一方、CQRS は、使用パターンに応じて読み取りモデルを再形成します。```text
Replica
  → aynı tablo
  → aynı join ihtiyacı
  → daha fazla okuma kapasitesi

Projection
  → ekran için hazırlanmış veri
  → daha az join
  → daha düşük latency ve I/O
```たとえば、順序リストで毎回 `CityId` を結合する代わりに、射影に `CityName` を書き込むことが非正規化の決定となります。この決定により、書き込み側に追加の同期コストが発生します。読み取り側では、1 回の読み取りで画面にフィードを送信できます。

## デュアル書き込み、送信ボックスおよび CDC

最も危険な流れは次のとおりです。```text
1. Siparişi veritabanına yaz
2. Broker'a event gönder
3. İkinci adımda sistem çöksün
```この場合、順序はありますが、イベントはありません。発送、通知、または予測は取り残されます。トランザクション アウトボックスはこのギャップを埋めます。ビジネス データと発行されるイベントは同じローカル トランザクションに書き込まれます。その後、発行者または CDC ベースのプロセスが送信トレイ レコードをブローカーに移動します。

|基準 |送信ボックス | CDC |
| --- | --- | --- |
|ドメインイベント制御 |アプリケーションが | を決定します。 DB が変更を決定 |
|イベントの意図 |開く |間接的 |
| DB スキーマの依存関係 |下 |高 |
|イベント内容 |ビジネス言語で設計 |テーブル/ログ構造の影響を受ける |
|マイクロサービスコンプライアンス |非常に高い |状況に応じて |
|設置費用 |その他のアプリケーション コード |インフラストラクチャに関する詳細情報 |

CDC は強力なツールです。これは、既存のシステムに変化をもたらす場合に特に価値があります。ただし、アプリケーションがドメイン イベントの意味を判断した場合、Outbox はより明確なコントラクトを生成します。

少なくとも 1 回の配信では、繰り返し配信が発生する可能性があります。したがって、コンシューマは冪等である必要があります。```text
DeliveryNoteUpdated event'i iki kez geldi
  → projection aynı version'ı gördü
  → ikinci işlem no-op oldu
```ここで冪等性は飾りではありません。これは、分散システムにおけるデータの一貫性を実質的に保証するものです。

## クラウドコスト: スライスが請求書に影響するのはなぜですか?

垂直スライスは単なるコード構成ではありません。また、スケールの単位も表示されます。チェックアウトの書き込み側が CPU を集中的に使用し、カタログの読み取り側がメモリとキャッシュを集中的に使用する場合、両方を同じリソース プロファイルに強制すると無駄が生じます。```text
Checkout command side
  → düşük concurrency
  → yüksek tutarlılık
  → transaction ve fraud kontrolü

Catalog query side
  → yüksek concurrency
  → cache ve projection
  → düşük latency beklentisi
```非対称ロードには非対称リソースが必要です。ただし、各キャッシュ、NAT、ロード バランサー、キュー、読み取りストア、および個別のデプロイ ユニットにより、請求書に新しい項目が追加されます。したがって、アーキテクチャ上の決定は、p95 レイテンシー、読み取り/書き込み比率、接続飽和、出力、再試行量、および入射爆発半径などの実際の指標に基づいて行う必要があります。

## パフォーマンスとメモリの分析

スライスのホット パスにより、不必要な割り当てが発生してはなりません。リクエスト モデルをドメイン エンティティに直接変換する代わりに、ハンドラーは必要なフィールドのみを移動します。クエリ側で集計を読み込むのではなく投影を読み取ることで、CPU、メモリ、および I/O のコストが削減されます。```text
Command path
  → tek aggregate
  → kısa transaction
  → sabit event sayısı

Query path
  → ihtiyaca göre projection
  → minimum column set
  → pagination veya cursor
```ページ サイズが `p` の場合、リスト クエリの複雑さは O(p) です。カーソルのページネーションでは、メモリは O(p) のままです。リスト全体を保存すると O(n) メモリが消費され、運用環境に不必要なリスクが生じます。

## テスト戦略

垂直スライス テストでは、モックの数ではなく、行動の信頼度を最適化する必要があります。

1. バリデーターテストにより、空白のメモ、最大長、禁止文字などの限界値が証明されます。
2. ハンドラー テストでは、成功したフローとエラー フロー (注文なし、所有権エラー、リクエストの重複) を検証します。
3. ドメイン テストでは集計の不変条件が保持されます。キャンセルされた注文にメモを追加することはできません。
4. 統合テストにより、トランザクションと送信トレイのレコードが同時に発生することが証明されます。
5. アーキテクチャ テストでは、スライスが別のスライスの内部クラスに依存していないことを確認します。
6. 投影テストは、イベントが再び発生した場合でも結果が冪等のままであることを示しています。

テスト ピラミッドの目的は、各クラスを分離することではありません。決定が覆されるポイントを素早く見つけることです。

## トレードオフ

垂直スライスでは、より小さなファイルが生成されます。これは意識的な価格です。その結果、変更の理由、テスト カバレッジ、ロールバック サーフェイスがより明確になります。

誤って適用すると、2 つのリスクが生じます。 1 つ目は、各スライスが独自のミニフレームワークを生成することです。 2 つ目は、`Shared` フォルダーが無限に増大し、古い階層化モノリスの新しい名前になることです。ソリューションは、狭いインターフェイス、オープン ポート、アーキテクチャ テスト、および ADR による意思決定記録です。

## よく混同される```text
❌ Vertical Slice = controller'dan repository'ye kadar her şeyi aynı klasöre koymak
✓ Kullanıcı niyetinin davranış, veri ve test sınırını birlikte tasarlamaktır.

❌ CQRS = mutlaka ayrı veritabanı
✓ CQRS önce niyet ayrımıdır; fiziksel ayrım ayrı bir maliyet kararıdır.

❌ Mediator = mimari
✓ Mediator yalnızca dispatch ve pipeline aracıdır; mimari sınırı feature sahipliği belirler.

❌ Outbox = exactly-once garantisi
✓ Outbox event kaybını önler; consumer yine idempotent olmalıdır.

❌ Read replica = projection
✓ Replica aynı modeli çoğaltır; projection okuma ihtiyacına göre yeni model üretir.

意思決定チェックリスト

  1. スライスは単一のユーザーの意図を表しますか?
  2. ハンドラーはアプリケーション オーケストレーションのみを実行しますか、それともドメイン ルールを保存しますか?
  3. コマンドとクエリは同じ応答パターンを共有する必要がありますか? それともそれらは分離されている方が明確ですか?
  4. トランザクション境界は単一の集合内にとどまりますか、それとも意識的な一貫性境界内にありますか?
  5. イベント ブロードキャストは、送信ボックスを介してデータ レコードと同じローカル トランザクションに接続されていますか?
  6. コンシューマは、重複したイベントを受信したときに冪等性を保ちますか?
  7. プロジェクションは本当にディスプレイの必要性を減らしますか? それとも不必要な運用コストを追加しますか?
  8. キャッシュ、仲介、読み取りストア、およびデプロイを個別に行う決定に関して、p95 のレイテンシ、コスト、または爆発範囲に関する証拠はありますか?
  9. インターフェイスは狭いですか? それとも、IOrderService のように、あらゆるユースケースを運ぶバッグになってしまったのでしょうか?
  10. アーキテクチャ テストでは、コンパイルまたは CI 段階で機能の境界が維持されますか?

これらの質問に対する答えが明確でない場合は、多くの場合、インフラストラクチャを追加するよりもスライス境界を単純化する方がコストが安くなります。

この記事で覚えておくべきこと

  1. 垂直スライスは、リクエストからテストまでの単一ユーザー インテントの動作制限です。
  2. CQRS、Mediator、Outbox、および Projection は同じ決定ではありません。それぞれが異なるコストとセキュリティの問題を解決します。
  3. 最初に論理的なフィーチャの境界を検証することで、物理的な分離のコストを制御します。
  4. 分散システムにおける送信ボックスのイベント損失を軽減します。冪等性により、再配信による損害が防止されます。
  5. クラウドのコストはアーキテクチャの外部ではありません。各境界は、コンピューティング、I/O、ネットワーク、および操作の項目になります。

エンドツーエンドのフロー```text

POST /orders | v Endpoint / Controller | v Mediator.Send() | v Validation | v Authorization | v Transaction | v Command Handler | v Aggregate | v Repository | v Outbox | v Commit | v Broker / Kafka | v Projection | v Read Database | v GET /orders | v Query Handler | v Frontend


次のセクションでは、運用環境で垂直スライス、DDD アグリゲート、およびモジュラー モノリス境界を一緒に維持する方法を検討します。

FAQ

よくある質問

「垂直スライス=コントローラーからリポジトリまでを同じフォルダに入れる」でよろしいでしょうか?

ユーザーの意図の動作、データ、テスト境界を一緒に設計することです。

「CQRS = 必然的に別のデータベース」は正しいですか?

CQRS は意図優先の区別です。物理的な分離は、別途コストを決定する必要があります。

「内側からの垂直スライスはどのように機能するのか?」それは何と言っていますか?

垂直スライスは、リクエストから検証、ハンドラーから集計、アウトボックス イベントからモデルの読み取り、そしてクラウド コストまで、内部的にどのように流れるのでしょうか? CQRS…

学んだエンジニアリング原則

  • スライスの実際の境界はフォルダーではなく、ユーザーの意図と一貫性の決定によって決まり、同じ理由で変化します。
  • CQRS と送信ボックスは物理的な分離の要件ではありませんが、意図と信頼性のコストを明示するツールです。
  • 実稼働アーキテクチャは、コード構成だけでなく、p95 レイテンシー、I/O、出力、再試行、ロールバックのコストによって測定する必要があります。

続きを読む

続きを読む

シリーズの次のシリーズ

関連記事

関連記事

Paylaş