asopi tech
asopi techIndie Developer
分割する技術

【2026年8月版】

分割する技術

公開日: 2026/08/20
読了時間: 約 53分

システム開発では、巨大なテーブルをパーティションに分け、大量のレコードをバッチに分け、複雑な業務をユースケースに分け、一つの処理を同期と非同期に分ける。連続的に変化している現実の状況も、有限個の状態に分けて記録する。プログラムを書きはじめる前から、設計はすでにどこで分けるかという問題に入っている。

分ければ扱いやすくなるという前提は、分けた内側にしか当てはまらない。分けた数だけ切断面が増え、そこには通信、結合、同期、整合性確認、例外処理が要る。内部の複雑さが減っても切断面の複雑さがそれを上回れば、システム全体はかえって扱いにくくなる。

0. 分割の基準と四つの対象

どこで分けるかを決める前に、何を基準に分けるのかが要る。基準がなければ分割は目についた単位で行われ、切断面の位置は偶然で決まる。ここでは分割の基準と、その基準が適用される四つの対象を先に置く。

小さくすることではなく境界を作ること

1972年にDavid Parnasが発表した論文は、モジュールを処理手順の順序で切るのではなく、変更され得る設計判断を隠すために切るという基準を示した。処理の流れに沿った分割は自然に見えるが、仕様が変わると複数のモジュールへ影響が及ぶ。判断を隠す分割なら、その判断が変わってもモジュールの外側は変わらない。この基準は現在のマイクロサービス、ドメイン駆動設計、データパーティショニングにもそのまま残っている。分割の目的は小さくすることではなく、独立して判断・処理・変更できる境界を作ることにある。

情報・問題・処理・状態

分ける対象は大きく四つに分かれる。情報の分割はカラムやレコードや期間やキーを対象にし、読み書きする量を限定するために行う。問題の分割は業務や判断や責任やルールを対象にし、理解できる大きさのビジネスロジックにするために行う。処理の分割は手順と依存関係と同期・非同期を対象にし、有効な時間内に結果を返すために行う。状態の分割は現実の状況と観測と時間を対象にし、アナログな世界を計算可能にするために行う。

四つは独立していない。設計者が業務をどこで分けるかを決めると、同時に守るべき不変条件の範囲が決まるため、トランザクションに含められる対象がその時点で確定する。トランザクションの範囲が確定すれば、同じトランザクションで更新する対象を同じ分割へ入れる必要が出るので、選べる分割キーは限られる。分割キーが決まれば、並列に実行できる処理の単位はその分割の単位と一致する。処理をどこで区切るかを決めれば、区切りごとに保存する状態と、結果を確定させる時点も決まる。したがって設計者は四つを別々に決められない。業務の分割を先に決めれば、残る三つは選べる範囲が狭まった状態で決めることになる。

四つの決定は次の順に連鎖する。

以降ではこの四つを順に扱い、最後にそれらを再結合する設計を扱う。

1. 情報の分割 ― 読み書きする量を限定する

情報の分割が決めるのは、一つの処理が読み書きする範囲である。カラム、レコード、パーティション、時間窓のどれを切っても、目的は同じ範囲の限定にある。切り方を決めるのはデータの形ではなく、その情報を使う判断の側になる。

判断ごとの属性の投影

多数のカラムを持つデータを前にしたとき、最初にテーブルを物理的に割るとうまくいかないことが多い。先に見るのは、その属性がどの判断に必要かという点になる。

「顧客」という概念には、氏名、連絡先、請求先、配送先、信用情報、購買履歴、マーケティング同意、サポート履歴が入り得る。しかし商品の配送処理に信用審査の詳細は要らず、問い合わせ対応に過去の決済トークンは要らない。すべてを一つの巨大な顧客データとして扱うと、どの処理も必要以上の情報へ依存することになる。

分割はユースケースが必要とする情報を投影するところから始まる。次に同じ理由で変更される属性をまとめ、更新頻度や保持期間が異なる属性を分け、アクセス権限が異なる属性を分ける。物理的な読み取り量が問題になった時点で、はじめて格納形式を最適化する。正規化は重複した事実や独立して変化する事実を分ける方法であり、分析処理では必要なカラムだけを走査するカラム指向の格納が効く。GoogleのDremelは、多段の実行ツリーとカラム指向レイアウトを組み合わせ、大規模なネストデータへの集約処理を実現している。

論理的な分割と物理的な分割は別のものとして扱う。ドメイン上は一つの概念でも、読み取りモデルでは複数の形を持ってよい。逆にストレージ上で一つのテーブルに入っていても、すべての処理がすべてのカラムを知る必要はない。

分割キーと処理コストの配分

大量のレコードは、範囲、ハッシュ、値の分類、所有者やテナント、時間、処理状態のいずれかで分けられることが多い。PostgreSQLのテーブルパーティショニングでも範囲・リスト・ハッシュの方式が提供され、公式ドキュメントの例では日付をパーティションキーに置いて、直近期間への問い合わせと古い期間の削除を効率化している。

均等に分かれるキーが良い分割キーとは限らない。効くのは、主要な処理が一つの分割内で完結し、同時に更新すべきデータが同じ分割内にあり、データ量と負荷が極端に偏らないキーになる。保持・削除・移行といったライフサイクルを局所化でき、障害が一部の分割に閉じ、将来の再分割が可能であることも条件に入る。

三分割したデータへ二種類の問い合わせを投げると、読む分割数は次のように入れ替わる。

問い合わせ顧客IDで分割日付で分割
一人の顧客の注文履歴1分割全分割
一日の全社集計全分割1分割

分割キーはデータベースの設定項目ではなく、どの処理を安くしどの処理を高くするかという業務上の優先順位の表明になる。

テーブル単位でサービスを切る分け方は、ここでずれる。テーブルは格納形式であって業務能力ではない。一つの業務判断が複数テーブルを使うことも、一つのテーブルが複数の読み取り用途に使われることもある。

件数による分割と意味の境界

「一万件ずつ処理する」「百件ずつページングする」という切り方は実装しやすい。ただし件数だけを基準にすると、意味のある境界が失われる。顧客別の請求額集計、商品別の在庫引き当て、契約単位の有効性判定を一万件ごとに分けたとして、チャンク境界をまたいで同じ顧客・商品・契約が現れれば、後から部分結果を再結合する処理が要る。

LIMITOFFSETによる分割も、安定した一意の順序を指定しなければ重複や欠落を起こす。PostgreSQLのドキュメントは、一意に定まるORDER BYがなければ取得対象が予測不能になり、大きなOFFSETでは読み飛ばす行もサーバー内部で計算されるため非効率になると説明している。

そのため大量処理では、「何件目から何件」よりも意味のある境界が使われる。主キーが前回値より大きい範囲、対象日時が特定範囲内、顧客IDのハッシュが特定範囲といった条件で切り、一つの契約や注文を途中で分断しない。チェックポイント以前のイベントを処理済みとする形もこれに当たる。チャンクサイズは分割そのものではなく、分割された単位を一度にどれだけ処理するかという実行上の調整値になる。「とりあえず千件」は性能調整値であって設計根拠にはならない。

終端のないデータの時間による区切り

イベントストリームには明確な終端がない。全体の合計を計算しようとしても、いつまで待てばよいかが決まらない。Apache Flinkのウィンドウ処理は、無限のストリームを有限のバケットへ分け、時間や件数の範囲ごとに計算可能にする。

ここでの分割には二つの軸がある。一つはキーで、顧客、端末、口座、注文など同じキーに属するイベントを同じ処理単位へ集める。もう一つは時間で、五分間、一時間、一営業日、セッション継続中のどこまでを一つの集計対象にするかを決める。Kafkaが保証する全順序も基本的にはパーティション内に限られるため、Kafkaのキーは負荷分散の指定であると同時に、どのイベント同士の順序を守るかという意味上の指定になる。

さらに、現実に出来事が起きた時刻、システムがイベントを受け取った時刻、どこまで到着したら結果を確定するかの三つを決めることになる。遅れて届いたイベントを無視するか、過去の結果を訂正するか、一定期間だけ待つかの選択がここに入る。ウィンドウは時間による分割であると同時に、不完全な情報をどの時点で十分にそろったとみなすかという業務ルールになる。

2. 問題の分割 ― 判断の単位で業務を切る

業務の分割単位になるのは、画面でもテーブルでもCRUD操作でもなく、業務上の判断である。判断を単位に取ると、誰が何を入力にどの規則で決め、何を必ず守るのかが一つずつ確定する。設計者は同じ規則が適用される判断をまとめて境界を引き、その境界の内側で守る不変条件から、一つのトランザクションに含める対象を決める。

分割単位にならないもの ― 画面・テーブル・CRUD

データや処理を分ける前に、問題そのものを分ける段階がある。ここで画面、テーブル、CRUD操作をそのまま業務の分割単位にすると、業務が何を判断し何を保証するのかが表現されない。顧客画面があるから顧客サービスを作り、注文テーブルがあるから注文クラスを作り、登録・更新・削除の三機能を作る、という分け方がこれに当たる。Controller、Service、Repositoryという技術レイヤーだけの分割も同じになる。一つのユースケースが多数のServiceを横断し、すべてが同じ巨大なデータモデルを共有しているなら、論理的には分割されていない。

問題を分けるときに見るのは、業務上の名詞よりも判断になる。受注業務であれば、注文を受け付けてよいか、商品を販売してよいか、在庫を確保できるか、支払いを受けられるか、どの納期を約束できるか、出荷を開始してよいか、売上として計上してよいかに分解できる。どれも「注文」に関係するが、同じモデル、同じトランザクション、同じタイミングで処理すべきとは限らない。

各判断について、誰が判断し、何を入力とし、どの規則を適用し、何を必ず守り、どの状態を変更し、何を外部へ通知するのかを明らかにする。いつまでに判断する必要があるかという期限と、判断できない場合の扱いも同じ段階で決まる。この形にすると、曖昧な「注文管理機能」が実装可能なユースケースに変わる。

境界の位置 ― 用語と不変条件の変化点

ドメイン駆動設計のBounded Contextは、大きなモデルを複数の一貫したモデルへ分け、相互の関係を明示する考え方になる。同じ「顧客」「商品」という語でも、販売、配送、会計、サポートでは意味が違う。大規模な業務全体を一つの統一モデルで表現することは、現実的でも経済的でもない。

境界の候補になるのは、同じ言葉の意味が変わる場所、責任を持つ部門や担当者が変わる場所、適用される規則や即時に守るべき不変条件が変わる場所である。情報の鮮度要件が変わる場所、障害時の復旧方法が変わる場所、変更される理由が変わる場所も同じ扱いになる。サブドメイン単位でのサービス分割でも、ビジネス能力やサブドメインを単位として、同じ理由で変わる機能をまとめることが勧められている。サービスを細かくするだけでは、自律性も変更容易性も得られない。

Bounded Contextをそのままマイクロサービスにする必要はない。先に要るのはコード上とモデル上の境界で、プロセスやデータベースを物理的に分けるのは、独立した展開、スケーリング、障害分離といった必要が出てからでよい。境界が不明確なまま物理的に分けると、単一プロセス内の関数呼び出しがネットワーク通信に変わり、単一トランザクションで済んでいた更新が分散整合性の問題に変わる。

「ビジネスロジック」という言葉にも性質の違う処理が混ざる。在庫数を超えて引き当てない、解約済み契約へ請求しない、承認権限を持たない利用者は承認できないといった業務上の判断と不変条件がドメインロジックになる。入力を受け取って対象を取得し、ドメインの判断を呼び出して保存し、イベントを発行して結果を返す進行制御はアプリケーションロジックになる。データベース、メッセージブローカー、外部API、ファイルとの接続はインフラストラクチャロジックにあたる。ヘキサゴナルアーキテクチャは、アプリケーション内部のユースケースを外部技術から分離し、ポートとアダプターを通じて接続する。この分離がないと、在庫を引き当てられるかという業務判断の中にSQL、HTTP通信、リトライ、画面表示用メッセージが同居し、業務ルールを変えるだけでデータベースや通信方式まで理解する必要が出てくる。

不変条件とトランザクション境界

分けるときに最優先されるのは、絶対に同時に守らなければならない条件になる。口座残高が利用可能額を下回らない、同じ座席を二重販売しない、同じ在庫を複数注文へ割り当てないといった条件である。Pat HellandはLife Beyond Distributed Transactionsで、分散トランザクションに依存しない設計を論じる中で、エンティティを一意なキーを持ち一つのトランザクションスコープ内に存在するデータの集合として捉えている。強く整合させる必要がある情報は、なるべく同じ境界内へ置くことになる。

在庫を確保した後に決済が失敗する場面で、境界の内側と外側を比べる。

内側では、確保も注文も同じトランザクションに入っているため、失敗した時点でどちらも自動的に元へ戻る。外側では確保の印が在庫サービスに残り続け、取り消しを送って、それが届いたかを照合するまで戻らない。

境界を越えた処理では、すべてを一度に確定する形が取れない。仮確定状態を持ち、メッセージで次の処理を開始し、重複実行を許容し、失敗時に補償し、後から照合する設計へ変わる。Sagaパターンは、複数サービスにまたがる業務処理を複数のローカルトランザクションと補償処理へ分ける。ただし自動ロールバックや完全な分離性が得られるわけではなく、補償と競合対策は明示的に設計することになる。分割によってトランザクションがなくなるわけではなく、単一の原子的トランザクションが途中状態、メッセージ、再試行、補償、照合へ分解される。

3. 処理の分割 ― 期限内に結果を返す

処理の分割で短くなるのは、処理の総量ではなく応答までの時間である。削れるのは最も長い依存経路の上にある待ち時間だけで、経路の外へ移した仕事は消えずに別の場所で動き続ける。分割の基準になるのは業務上の期限で、その期限は下位の処理へ配分される。

クリティカルパスと期限からの逆算

応答時間を決めるのは処理の個数ではなく、最も長い依存経路の長さになる。そのため処理をコード上の関数ではなく、時間を消費する仕事の連鎖として見ることになる。

高速化のためにスレッド数やワーカー数を増やす前に、処理間の依存関係を整理する順番が効く。注文受付処理なら、入力の検証、顧客の購入可否確認、商品の販売可否確認、在庫の引き当て、決済の承認、注文の確定、確認メールの送信、分析用イベントの送信に分解できる。顧客と商品の確認は並列に実行できる可能性があり、在庫引き当てと決済承認も業務上許されるなら仮確保として並列化できる。注文の確定は必要な判断がそろうまで開始できず、確認メールと分析イベントはレスポンスを返した後へ回せることが多い。

この依存グラフの上で応答時間を決めるのは、最も長い経路、つまりクリティカルパスになる。十個の処理のうち九個を並列化しても、残る一個が全体時間の大半を占めていれば応答時間はほとんど変わらない。並列化できない部分が全体の高速化を制限するという議論は、Amdahlが1967年に示したものとして知られている。

注文受付の三つの処理を同じ時刻から始め、横軸に時間を取ると、この関係が見える。

三本とも同じ start の線から伸びるが、顧客の確認と商品の確認は先に終わり、外部APIを呼ぶ決済の承認だけが伸び続ける。下段の応答時間が止まるのは、最後の一本が終わった時点になる。速い二つをさらに速くしても、この位置は動かない。

処理設計では、速くするより先に期限を決める。ユーザーへの応答は2秒以内、在庫確保は500ミリ秒以内、日次集計は翌朝6時まで、不正検知は決済確定前、メール送信は5分以内というように業務上有効な期限を置き、全体の期限を各処理へ配分する。外部API呼び出しを含む処理では、平均値だけでなく遅い側の分布を見ることになる。多数の下位処理を呼び出すサービスでは、一部の遅延が全体の待ち時間を支配する。GoogleのThe Tail at Scaleは、大規模サービスにおけるテールレイテンシーの影響とその抑制を論じている。

処理ごとに見る値は、p50、p95、p99に加えて、タイムアウト率、再試行率、キュー待ち時間、実処理時間、外部依存先ごとの時間になる。応答時間は計算時間だけで決まらず、待ち時間、ロック待ち、接続待ち、キュー待ち、再試行待ちが含まれる。

非同期化による待ち時間の移動

非同期化そのものは高速化ではない。短くできるのは、現在の応答を作るために待つ必要がない処理だけになる。判断の基準は、その処理の結果が今返そうとしている応答の内容を変えるかどうかに絞られる。変えるなら原則としてクリティカルパス上に残り、変えないならキューへ移せる可能性が出てくる。注文受付で分類すると次のようになる。

処理同期で必要か
入力形式の検証必要
商品を販売できるか必要
在庫を確保できるか多くの場合必要
決済を受け付けたか業務モデルによる
確認メールの送信通常は不要
分析データへの反映不要
検索インデックス更新多くの場合不要

非同期に移した処理には新しい責任が付いてくる。メッセージを失わないこと、重複と順序の入れ替わりを処理すること、途中状態を利用者へ説明すること、失敗を検知して再試行回数を制限すること、最終的に整合したかを照合することが要る。処理量そのものは減らないため、遅い処理をすべて非同期にしても処理能力不足は解消しない。待ち時間が利用者から見えない場所へ移るだけになる。

キューの限界と過負荷時の動作

キューを置いたことで処理できる量が増えるわけでもない。増えるのは一時的な負荷変動を吸収し、処理速度の違いを調整できる余地である。処理能力より速く仕事が到着し続ければ、キューは増え続ける。Reactive Streamsは、速い送信側が遅い受信側を圧倒しないよう、非同期境界を越えてバックプレッシャーを伝えてキューを有限に保つことを中心課題にしている。

そのためキューには、件数、総データ量、待ち時間、一件当たりの試行回数、全体の再試行量、同時実行数の上限が要る。限界を超えたときの動きも先に決まっている必要がある。新規受付を拒否する、優先度の低い処理を捨てる、簡易処理へ切り替える、入力側を減速させる、古くなった仕事を失効させる、人手による確認へ送るといった選択肢がある。GoogleのSRE本のカスケード障害の章でも、過負荷時にキュー長や同時処理数を制限し、負荷遮断や縮退動作でシステム全体の崩壊を防ぐ方法が説明されている。十秒待ったレスポンスを利用者がすでに必要としていないなら、その処理を最後まで続けても価値は生まれず、後続のリクエストがさらに遅れる。

期限とキャンセルの下位への伝播

上位処理の期限が切れた後に下位処理が動き続けると、資源だけが消費される。画面への応答期限が二秒で、すでに1.5秒を使っているなら、後続サービスへ与えられる残り時間は0.5秒になる。gRPCのdeadlineは、クライアントがいつまで応答を待つかを指定し、下位呼び出しへ期限を伝播させる仕組みを持つ。期限経過後に不要な処理を続けないことは、レイテンシーだけでなく計算資源の面でも効く。

キャンセルできるようにするには、処理を適切な中断点で分けておく。一件ごと、一チャンクごと、外部呼び出しの前後、永続化の前後、ステージの境界、チェックポイントの境界が中断点になる。巨大な一処理として実装されていると、期限切れを検出しても安全に止められない。

再送に耐える更新処理

分散処理では、要求が成功したのに応答だけが失われることがある。クライアントは成功したかどうか判断できず、同じ要求を再送する。そのため更新処理には冪等性を持たせる。操作ごとに一意な冪等キーを発行し、処理済みキーを記録し、同じキーには同じ結果を返し、副作用を重複させず、キーの有効期限を明示する。AWS Well-Architectedの信頼性の柱でも、変更操作へ冪等トークンを付けることで、同一要求が複数回到着しても重複レコードや重複副作用を出さずに再試行できる設計が推奨されている。

再試行の担当階層

再試行を階層ごとに勝手に行うと、障害中のシステムへ要求が増幅される。クライアント、API、サービス、ワーカー、データベース接続がそれぞれ再試行すれば、一回の失敗が大量の要求に変わる。どの階層が担当し、最大何回まで、どのエラーを対象に、どの間隔で行い、全体期限内に収まるかを一つの方針として決める形が取られる。

4. 状態の分割 ― 連続する現実を有限の状態へ落とす

現実は連続的に変化し、その変化はシステムへ遅れて、しかも不完全に届く。状態の分割は、この現実を有限の状態・イベント・時刻へ変換する作業になる。ここで決めるのは状態の名前ではなく、どの遷移を許すか、いつの時点の事実として記録するか、後から訂正できるかである。

業務状態・観測状態・処理状態の分離

現実世界はデータベースのようには動かない。荷物はシステムが「配達済み」に更新した瞬間に配達されるわけではなく、先に荷物が渡され、後から端末で読み取られ、通信され、サーバーで処理される。設備も正常から故障へ一瞬で切り替わるとは限らず、温度、振動、電圧、応答時間が徐々に変化し、複数の兆候を経て故障と判断される。契約は有効と無効だけでは表現できず、申込中、審査中、承認済み、発効待ち、有効、変更手続き中、解約予約済み、終了、取消、状態不明といった段階を持つ。現実のデジタル化は、連続的で曖昧な現実を、システムが判断できる有限の状態、イベント、数値、時刻へ変換する作業になる。

荷物が14時02分に手渡され、端末が14時09分に読み取り、サーバーが14時15分に反映したとする。手渡しから反映まで13分あり、その間システムは配達済みを知らない。「配達済み」という一つのカラムには、少なくとも三つの事実が混ざる。現実に荷物が渡されたこと、配達員が端末へ入力したこと、サーバーが入力を処理して状態を更新したことで、三つの時刻は一致しないことがある。そのため重要な業務では、荷物や契約といったエンティティをどう扱うべきかという業務上の状態と、センサーや担当者や外部サービスが何を報告したかという観測された状態と、システムが受信・検証・反映・通知をどこまで完了したかという処理上の状態を分ける。

業務状態: 配達済み
観測状態: 配達員による受領サイン取得
処理状態: 配達イベント反映済み、通知処理待ち

この分離がないと、メール送信に失敗しただけなのか、通信が遅れているだけなのか、入力内容に異議があるのかを区別できない。

許されない遷移の定義

状態を正確に扱うには、状態名だけでなく遷移を定義する。基本要素は、現在状態、発生したイベント、遷移を許可する条件、遷移時に実行する処理の四つになる。W3CのSCXML仕様でも状態・遷移・イベントが状態機械の基本概念として定義され、イベントを受けたときに一致する遷移を選び、遷移に伴う処理を実行するモデルが示されている。

受付済み
  ├─ 在庫確保成功 → 確保済み
  ├─ 在庫確保失敗 → 保留
  └─ 取消要求     → 取消済み

確保済み
  ├─ 決済成功 → 確定
  ├─ 決済失敗 → 支払待ち
  └─ 取消要求 → 取消処理中

定義が要るのは、許される遷移だけではない。出荷済みから受付済みへ戻せるか、取消済み注文を再開できるか、同じ決済成功イベントを二度受け取ったらどうするか、支払失敗後に遅れて成功通知が来たらどうするかも決まっている必要がある。状態遷移の定義は、業務ルールをコード化する前に、業務が何を意味しているのかを確定する作業になる。

真偽値では表現できない状態

前節で定義した遷移の幅は、状態を真偽値だけで表現すると消える。is_activeis_paidis_deliveredis_approvedのようにtruefalseしか持たない形では、分からない、確認中、一部だけ完了、情報が古いといった状態がどちらかへ押し込まれる。端末の死活監視なら、正常応答あり、応答遅延、一時的な通信失敗、一定回数連続失敗、監視自体の停止、最終観測が古い、メンテナンス中を区別すべき場合がある。

区別できる状態を用意しても、判定の境界を決めなければ状態は安定しない。観測値が閾値付近を往復すると状態が正常と異常の間で頻繁に反転するため、異常判定と復帰判定で異なる閾値を使い、一定回数の連続観測を要求し、最低継続時間とデータの鮮度期限を設ける。ヒステリシス、デバウンス、タイムアウトと呼ばれる扱いになる。デジタル化は値を整数や文字列へ変換する作業ではなく、観測の不確実性と判断の境界条件を明示する作業になる。

変化の記録と訂正可能性

現在状態だけを上書きすると、なぜその状態になったのかが失われる。Event Sourcingは、アプリケーション状態への変更をイベントの列として保存し、過去状態の再構築や訂正後の再計算を可能にする。すべてのシステムをEvent Sourcingにする必要はないが、重要な状態変更については記録を残す価値がある。残す内容は、何がどの対象に起きたか、誰または何が報告したかになる。いつ現実に起きていつシステムが知ったかという二つの時刻と、どの規則で判断したかも要る。以前の状態と新しい状態、関連する操作ID、訂正元のイベントがそこへ加わる。

有効時刻と記録時刻の二軸

後から訂正され得る業務では、現実に有効だった時刻とシステムが記録した時刻を分ける必要が出てくる。Bitemporal Historyは、現実でいつ有効だったかと、システムがいつその事実を知っていたかという二つの時間軸を扱う。8月1日に発生した契約変更が8月5日に登録された場合は次のようになる。

effective_at = 2026-08-01
recorded_at  = 2026-08-05

単一のupdated_atだけでは、8月3日時点で実際に有効だった契約と、8月3日時点でシステムが知っていた契約を区別できない。分割、丸め、分類、集約によって失われた情報は後から再現できないため、訂正、監査、ルール変更があり得るなら元の観測やイベントを保存しておくことになる。

5. 分割と再結合 ― 切断面の代償を決める

分割は複雑さを消さず、内部から切断面へ移すだけである。したがって設計では、どこで分けるかと、分けたものをどう再結合するかを同時に決めることになる。分割の良し悪しを測る基準も、部品の小ささではなく切断面の扱いやすさに置かれる。

設計を始める位置 ― 最終的な業務判断

実際の設計は、最終的な業務判断を定義するところから始まる形が多い。「注文を登録する」ではなく「この注文を受け付けられるか判断し、受付結果を返す」と定義し、いつまでに結果が必要かという期限を置く。画面応答、機械制御、日次バッチ、月次締めでは、許容される時間が違う。次に絶対に破られてはならない不変条件を抽出すると、同一トランザクションに含めるべき対象が決まる。何を同一のエンティティとして扱うかという識別子もここで決め、注文ID、契約ID、顧客ID、端末ID、処理ID、イベントIDを混同しないようにする。

そのうえで、用語、責任、規則、不変条件が変わる場所に業務上の境界を置き、主要なユースケースが一つの分割内で完結するかを確認する。頻繁な横断問い合わせや分散更新が必要なら、分割キーを見直す。処理の依存グラフを作って並列にできる処理と順番が要る処理とレスポンス後に回せる処理を分け、どこから再実行するかという失敗単位を、一件、チャンク、ステージ、トランザクション、Saga全体のどこに置くかを決める。発生時刻、受信時刻、処理時刻、確定時刻を区別し、不明、保留、失効、取消、訂正といった状態も定義する。

分割対象ごとの再結合手段

情報を分ければ結合が要り、業務を分ければ責任の受け渡しが要る。処理を分ければ途中状態と再試行が要り、時間を分ければ遅延データと締め時刻の問題が出る。状態を分ければ複数の見方を整合させる必要が出てくる。分割設計では、分割方法と再結合方法を同時に決めることになる。

切断面に何が要るかは、何を分けたかで決まる。

分割対象再結合に必要なもの
カラムID、スキーマ、関連の意味
レコードキー、順序、重複排除
パーティションルーティング、集約、再配置
業務領域API、イベント、用語変換
処理ステージメッセージ、チェックポイント
トランザクション補償、照合、収束条件
時間窓ウォーターマーク、遅延許容
状態遷移規則、イベント履歴

良い分割では境界を越える回数が少なく、越えるときの契約が明確になっている。悪い分割では処理のたびに複数の境界を往復し、共有データを互いに書き換え、相手の内部状態を知る必要が出てくる。

収束条件としての整合性モデル

非同期処理や分散処理では、最終的に整合するという説明だけでは収束条件になっていない。いつまでに整合するのか、整合しなければどう検知するのか、どちらを正とするのか、誰が訂正するのか、利用者へどう見せるのかまで決めて、はじめて整合性モデルとして機能する。

分割の評価基準

分割の評価は部品の小ささでは測れない。変更を局所化できるか、一つの判断が一つの境界内で完結するか、守るべき不変条件を局所化できるかで決まる。負荷を偏りなく配分して期限内に処理できるか、失敗を限定して安全に再実行できるかも同じ基準に入る。分割された結果を正しく再結合でき、現実とのずれを検出して訂正できるかまで含めて、はじめて評価になる。分割しても複雑さの総量は減らない。内部へ閉じ込める複雑さと境界を越えて調整する複雑さを選び、切断面の代償を明示する作業が、システムを現実的な規模と時間と責任の中へ収めることになる。

参考リンク