第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