📑

第2章 RML-1 — Closed World の設計原則:安全に失敗できる部屋を作る

に公開

RML-1 — Closed World の設計原則:安全に失敗できる部屋を作る

『The Worlds of Distributed Systems』第2章


「まだ何も外には出ていないうちは、いくらでもやり直せる」

RML-1(Closed World)は、三つの世界の中でいちばん地味ですが、
ここがちゃんとしていないシステムは、だいたい全部つらくなる世界です

この章では、

  • RML-1 を「安全に失敗できる部屋」として捉え直し、
  • どこまでを RML-1 に閉じ込めるべきか、
  • どこで RML-2 / 3 に昇格させるべきか、

という設計の勘所を整理します


1. Closed World とは何か — 「部屋の外にまだ出ていない」状態

まず、RML-1 を一言でまとめると

「外部世界からまだ観測されていない一時的な世界」

です

1.1 RML-1 の条件

RML-1 に属する処理は、ざっくり次の条件を満たします

  • 外部DBの 書き込みがない
  • 外部APIを 叩いていない
  • メール/Slack/Push通知など、人間への通知が飛んでいない
  • ログも、「後から業務判断に使われる」ようなものはまだ出ていない

つまり、

「この処理が失敗しても、世界のどこにも痕跡が残らない」

状態です

1.2 典型例

  • 入力フォームのバリデーション(ブラウザ内)
  • 検索条件に基づくクエリの組み立て(まだ実行していない)
  • 推論エンジンでのスコア計算(結果を捨てる Dry Run)
  • 本番データを読みつつ、書き込みはしないシミュレーション

「部屋の中(RML-1)」でいくら試しても、外の世界(RML-2/3)の状態は変わりません

1.3 「サンドボックス」との違い

RML-1 の話をすると、よく

「それって、サンドボックス環境と何が違うの?」

と聞かれます

ざっくり言うと

  • サンドボックス: 隔離された「環境(Environment)」

    • 本番とは別 URL / 別クラスター / 別アカウント
    • その中では普通に DB 書き込みや外部連携が起きうる
  • RML-1: 隔離された「状態(State / Context)」

    • 同じ環境(ときには本番)でも、外部から観測されない状態だけを扱うモード

つまり、

  • staging / sandbox 環境の中にも RML-2 / RML-3 は存在しうる
  • 本番環境の中にも RML-1 を作ることができる

という関係です

サンドボックスは「場所」の概念、RML-1 は「振る舞い・状態」の概念

として切り分けておくと、議論がやりやすくなります


2. なぜ RML-1 を意識的に切るべきなのか

RML-1 を意識すると、実務的には次のようなメリットがあります

2.1 「安全に雑に試せる」場所になる

  • 実データを使った実験
  • 新アルゴリズムの比較
  • A/B テストの offline 側

などを RML-1 に閉じ込めると、

「まず雑に試す」
「気に入らなければなかったことにする」

が気軽にできます

逆に言うと、RML-1 が設計されていないと、

  • ちょっとした PoC がすぐ RML-2/3 まで漏れ出す
  • 「本番データで試すのは怖いから、しょぼいテストデータだけでやる」
    → モデルもロジックも育たない

という悪循環にハマりがちです

2.2 「世界の境界」を目で見えるようにできる

RML-1 をはっきり切ると、

「ここから先は外の世界に出ていく」

という境界が明確になります

  • トランザクションの開始地点
  • キューへの publish
  • HTTP リクエストの送信

などを **「RML-1 → RML-2 への出口」**として意識できるようになります

設計レビューで

「この関数の中では、まだ RML-1 を守ろう」
「ここから先は RML-2 なので、エラー処理とロールバックをちゃんと考えよう」

という会話がしやすくなります

2.3 テストと staging の意味が変わる

「テスト環境」とか「staging」とかは、
本来 RML-1 を支えるための道具ですが、

  • 本番DBを直参照している
  • 外部サービスにも普通に繋がっている
  • メールや通知も普通に飛ぶ

といった「なんちゃって staging」だと、世界的にはもう RML-2 / 3 に踏み込んでいます

「環境」ではなく「世界」単位で考える

ようになると、

  • 本番環境の中にも RML-1 を作る(Dry Run API など)
  • staging でも一部だけ RML-2/3 を禁じる

といった設計がやりやすくなります


3. RML-1 の設計パターン

ここから、Closed World をちゃんと作るための
具体的なパターンをいくつか見ていきます

3.1 Read-Only + Dry Run パターン

アイデア

「読み取りは本番を使うけど、書き込みと通知は絶対にしない」

という世界を作る

3.1.1 典型ユースケース

  • 本番データを使った新アルゴリズムの評価
  • 「このルールを本番で適用したら、どれくらい影響が出るか?」の可視化
  • バッチ処理の Dry Run(ログだけ出す)

3.1.2 実装のイメージ

  • DB 接続は READ ONLY 権限のロールを用意
  • 外部APIクライアントは “no-op mode” を持つ

擬似コード:

type World = "RML1" | "RML2" | "RML3";

type Env = {
  world: World;
  db: DbClient;           // read-only かもしれない
  notifier: Notifier;     // RML-1 では no-op
};

async function simulate(env: Env, input: Input): Promise<SimulationResult> {
  if (env.world !== "RML1") {
    throw new Error("simulate は RML-1 専用です");
  }

  const raw = await env.db.fetchProductionData(input);
  const result = runNewAlgorithm(raw);
  // ここでは result を返すだけ保存・通知はしない
  return result;
}

ポイント

  • コードレベルで「world を見て振る舞いを変える」癖を付けておく
  • RML-1 では notifier.send(...)db.update(...) がエラーになる or no-op になる

3.2 「出口を一箇所に集約する」パターン

RML-1 の世界から RML-2/3 へ出る境界(=外部効果の発生地点)を、
できるだけ少ない場所に集約します

3.2.1 アンチパターン

  • サービスのあちこちで勝手に HTTP を叩く
  • ライブラリの中でさらっとメールを送る
  • ビジネスロジックの中から直接キューに publish する

こうなると、

「この処理は本当に RML-1 なのか?」

を判断するのが、とても難しくなります

3.2.2 パターン:Effect Dispatcher

外部効果をすべて 「Effect」として列挙し、最後に一括で実行する

type Effect =
  | { type: "SendEmail"; to: string; subject: string; body: string }
  | { type: "UpdateDb"; table: string; id: string; payload: any }
  | { type: "EmitEvent"; topic: string; payload: any };

type World = "RML1" | "RML2" | "RML3";

async function runBusinessLogic(input: Input): Promise<{ result: Result; effects: Effect[] }> {
  // ...計算・検証など(ここは RML-1 相当)
  const effects: Effect[] = [];

  // 何かの条件でメールを送ることにした
  effects.push({
    type: "SendEmail",
    to: input.userEmail,
    subject: "〜〜のお知らせ",
    body: "……",
  });

  // DB 更新もここに積む
  // effects.push({ type: "UpdateDb", ... })

  return { result, effects };
}

async function commit(world: World, effects: Effect[]) {
  if (world === "RML1") {
    // Dry Run: 実行せずログだけ出す
    console.log("DryRun effects:", effects);
    return;
  }
  // RML-2/3 の世界では、ここで初めて外部効果を実行
  for (const e of effects) {
    // Effect の種類ごとに実際の処理
  }
}

こうすると

  • runBusinessLogic の中は RML-1 の世界に保ちやすい
  • commit が「世界の境界」「責任の境界」になる

設計レビュー時に、

「この関数ではまだ Effect を積むだけですよね?」

と確認できるのが大きいです


3.3 本番環境での RML-1 機能 — 「安全なチートモード」

もう一歩踏み込むと、

「本番環境の中に RML-1 を埋め込む」

という発想もあります

3.3.1 例:安全な「お試しシミュレーション」

  • 本番ユーザーが、まだ保存されない preview を見られる
  • 管理者が「この修正を本当に適用するとどうなるか」をシミュレーションできる

UI 的には

  • 「保存」ボタン:RML-2/3 への出口
  • 「シミュレーション」ボタン:RML-1 のまま戻る

という感じです

3.3.2 例:安全な「ロールフォワード検証」

  • 新しい決済ロジックを本番トラフィックに対して走らせる
  • しかし結果は RML-1 の世界に閉じ込め、RML-2/3 は旧ロジックのまま

いわゆる shadow / ghost deployment 的なやつを、「世界の観点」で綺麗に説明できます


3.4 コラム:RML-1 と冪等性/副作用のグラデーション

上級者からは、こんな質問が出てくるかもしれません

「書き込みがあっても、完全に冪等なら RML-1 に近い扱いができるのでは?」

結論から言うと、この本ではあえて

RML-1 は「外部世界に対する副作用ゼロ(No Side Effect)」を原則とする

と割り切っています

理由はシンプルで

  • 冪等かどうかの判定自体が難しい
  • 「冪等だから安全」と思っていたものが、実は観測されていた、はよくある

からです

ただし、実務上は少しグラデーションがあります

  • ローカルキャッシュの更新
  • 一時ファイルの生成(後から誰も参照しない)
  • 同一プロセス内だけで見えるメモリキャッシュ

のように、

「外部システムや人間からは観測不能な副作用」

であれば、RML-1 の内側の実装ディテールとして許容してもよいでしょう

逆に

  • 別プロセスから参照される共有キャッシュ(Redisなど)
  • 別サービスが参照する一時テーブル
  • 運用や法務が後から見る可能性のあるログ

は、たとえ書き込みが冪等設計だったとしても、

「外から観測されうる時点で、もはや RML-2/3 側の世界」

と扱った方が安全です

実務上の指針としては

  • RML-1 = 「外部から観測不能な副作用だけが許される世界」
  • RML-2 以降 = 「他者から観測される副作用がある世界」

と定義しておくと、境界を引きやすくなります


4. RML-1 を壊すありがちな落とし穴

RML-1 は一見単純ですが、現実のコードでは壊れがちです

4.1 「ログくらいならいいでしょ問題」

「どうせログに出すだけだし、RML-1 でいいよね?」

と考えがちですが、ログにも種類があります

  • 純技術的なログ(例:実行時間、内部状態)
    → RML-1 の範囲でも比較的安全
  • ビジネス上の意味を持つログ/監査ログ
    → 後から参照されるなら、それは RML-3 に近い

「ログに書く = History World に刻む」ことになる場合は、
それはもはや RML-3 的な振る舞いです

指針

  • RML-1 では、後から業務判断に使われないログに限定する
  • 監査ログ・取引ログは RML-3 として扱う

4.2 「stagingだからOK」幻想

staging だからといって、世界が変わるわけではありません

  • staging でも外部決済ゲートウェイに繋がっている
  • staging でも本物のメールが飛ぶ
  • staging でも real partner にリクエストを投げている

のであれば、そこは RML-2/3 の世界です

RML-1 と staging の違い

観点 RML-1 staging 環境
データ 本番 or テストだが、書き込みなし 多くはテストデータだが、書き込みあり
外部連携 原則なし / no-op つながってしまうことも多い
世界としての扱い 「痕跡を残さない部屋」 「別世界」だが、痕跡は残る

結論

  • staging を RML-1 だと思っていると、危険
  • 本番の中に「RML-1 モード」を作る方が、むしろ分かりやすいこともある

4.3 「ついでに通知を飛ばしたくなる」問題

Dry Run やシミュレーション機能を作っているときに、

「せっかくだから、この結果をメールで共有できるようにしません?」

という魔の誘惑が来がちです

  • それを許した瞬間、世界は RML-2 / 3 に昇格する
  • 「シミュレーションだけのつもりが、人間の現実行動につながる」

対処法

  • 「RML-1 の機能では、外部通知は絶対に許可しない」と決めておく
  • 必要なら「結果をコピーして、RML-2/3 の画面で共有する」という二段構えにする

5. 「RML-1 だと思ってたけど実は違う」を検査する

実務上、いちばん怖いのは

「RML-1 だとみんなが思っている」
のに、実は RML-2/3 の振る舞いをしてしまっている

ケースです

5.1 チェックリスト

対象の処理について、次の質問に YES が付いたら要注意です

  • この処理の結果、誰かの意思決定が変わるログが残るか?
  • 他サービスが参照する DB に何かを書き込むか?
  • 過去1週間以内、この処理で発生したデータについて問い合わせが来たことがあるか?
  • この処理の挙動を、規約やSLAで説明する必要があるか?

YES がひとつでもついたら、

「これ、RML-1 じゃなくて、少なくとも RML-2 じゃない?」

と疑ってみる価値があります

5.2 実務的な落とし所

いきなり境界を完璧にするのは難しいので、

  • 「明らかに RML-1 にしてよい場所」を増やす
  • 「明らかに RML-1 じゃない場所」にラベルを貼る

という、極端から埋めていくアプローチが現実的です


6. 実務での使い方 — 「RML-1 ラベル」をプロジェクトに貼ってみる

最後に、RML-1 の世界観を現場に持ち込む簡単なやり方を

6.1 機能単位でラベルを付ける

機能一覧やエピックに対して、

  • RML-1 strict(絶対に外部効果を持たない)
  • RML-1 read-only(本番読み取りだけ)
  • RML-2+(どこかで外部効果がある)

のようなラベルを付けてみます

機能 RMLラベル コメント
スコアリングエンジンのシミュレーション RML-1 read-only 本番DB読取のみ、書き込みなし
A/Bテストの自動アサイン RML-2+ ユーザー属性テーブルを書き換える
エクスポート用CSVプレビュー RML-1 strict 画面表示のみ、ファイル生成なし

議論の際に、

「これ、RML-1 strict にしません? 外部効果は別の機能に切り出して」

といった提案がしやすくなります

6.2 コードベースで「RML-1 専用モジュール」を作る

  • world = RML1 を前提に動くモジュール群
  • そこからは、外部クライアントには直接触れない

というレイヤをひとつ作るのも手です

app/
  world_rml1/
    scoring/
    validation/
    simulation/
  world_rml2/
    saga/
    effect_dispatcher/
  world_rml3/
    ledger/
    incident/

ここまできっちり分けなくても、

  • 「このディレクトリ配下は RML-1 前提ね」

という暗黙ルールがあるだけで、だいぶ設計が変わります


7. まとめ — 「安全な部屋」をちゃんと作る

この章で見てきたことを、ざっくりまとめると

  • RML-1 = 外部世界からまだ観測されていない、一時的な世界

  • RML-1 がきちんとあると

    • PoC や実験を本番データで安全に試せる
    • RML-2 / 3 への出口(責任の境界)が見えやすくなる
  • 典型パターン:

    • Read-Only + Dry Run
    • Effect dispatcher による「出口の集約」
    • 本番内 RML-1 機能(シミュレーション・プレビュー)
  • 落とし穴:

    • ログが実は RML-3 になっている
    • staging を RML-1 だと勘違いする
    • シミュレーション機能に通知や書き込みを混ぜる
  • 冪等性については

    • RML-1 は原則として「外部から観測不能な副作用しか持たない世界」と定義し、
    • 共有キャッシュや監査ログに触れた時点で RML-2/3 側とみなす

RML-1 をきちんと設計することは、単に「テストがしやすくなる」という話ではなく、

「どこから先が、本当に世界に影響を与えるのか?」

をチームで共有するための、基礎工事です

次の第3章では、この RML-1 の部屋から一歩だけ外に出て、

RML-2 — Dialog World(対話の世界)

として、サービス同士・人間とのやりとりをどう扱うかを見ていきます

Discussion