あなたが作ったシステムは、どう壊れますか?
この質問に即答できるエンジニアは少ないです。私も以前は「壊れないように作る」ことばかり考えていました。でも現実には、システムは必ず壊れます。外部APIは落ち、DBは詰まり、ネットワークは切れる。問題は壊れるかどうかではなく、どう壊れるかです。
壊れ方には3つの設計思想があります。Fail Fast(早く壊れる)、Fail Safe(安全に壊れる)、Fail Loud(大きな音で壊れる)。この3つを意図的に使い分けるだけで、障害対応の風景がまったく変わります。
3つの壊れ方 -- 同じ障害、違う設計
外部APIが応答しない。この1つの状況に対して、3つの壊れ方は全く異なる振る舞いをします。
// Fail Fast: 即座にエラーを返す
async function getUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`, { timeout: 1000 });
return response.json();
} catch (error) {
throw new Error('User data unavailable');
}
}
// Fail Safe: 代替データで継続する
async function getUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`, { timeout: 1000 });
return response.json();
} catch (error) {
return { id: userId, name: 'Unknown User' };
}
}
// Fail Loud: アラートを発砲してからエラーを返す
async function getUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`, { timeout: 1000 });
return response.json();
} catch (error) {
alerting.send('CRITICAL: User API down');
logger.error('User API failure', { userId, error });
throw new Error('User data unavailable');
}
}
どれが正解かはコンテキストで決まります。決済処理ならFail Fast、表示名ならFail Safe、認証ならFail Loud。重要なのは「どの壊れ方を選んだか」を意識的に判断していることです。
Fail Fast -- 悪いデータを遠くまで運ぶな
異常を検知したら、可能な限り早くエラーを出す。Jim Shoreが2004年のIEEE Software誌で提唱した設計思想です。要するに「腐った卵は冷蔵庫に入れる前に捨てろ」です。
// NG: 異常データが奥深くまで侵入
function calculatePrice(product) {
const basePrice = product.price;
const tax = basePrice * 0.1;
if (basePrice < 0) { // ここで初めて発覚(手遅れ)
throw new Error('Negative price');
}
return basePrice + tax;
}
// OK: 入口で即座にチェック
function calculatePrice(product) {
if (!product || typeof product.price !== 'number') {
throw new Error('Invalid product data');
}
if (product.price < 0) {
throw new Error('Price cannot be negative');
}
return product.price * 1.1;
}
異常データが深い場所で検知されると、どこで混入したかわからない、途中で他のデータを汚染している、巻き戻すべき処理が多すぎる -- 三重苦になります。入口で弾けば、これらは全部起きません。
認証系で最も危険なのは「静かに成功を返す」コードです。
// 最悪のパターン: エラー時に認証成功を返す
async function checkSession(sessionId) {
try {
const session = await redisClient.get(sessionId);
return session ? JSON.parse(session) : null;
} catch (error) {
return true; // Redisが死んだら全員ログイン成功(!)
}
}
Redisが落ちた瞬間、全ユーザーが認証をバイパスできてしまいます。Fail Fastで書き直せば、Redisダウン時は即座に認証失敗になり、問題がすぐ発覚します。
Fail Safe -- 信号機が故障したら全方向赤点滅
障害が発生した時、システムが安全な状態に移行する設計です。物理世界から学べます。
信号機が故障したらどうなるか。緑で固定されたら事故が起きます。正解は全方向赤点滅(一時停止)。交通は遅くなりますが、安全は確保されます。
ソフトウェアでの実装は「デフォルト拒否」です。
function checkPermission(userId, resource) {
try {
return permissionService.hasAccess(userId, resource);
} catch (error) {
logger.error('Permission check failed', error);
return false; // エラー時は拒否(安全側に倒す)
}
}
金融システムではトランザクションのロールバックがFail Safeです。送金処理の途中でエラーが起きたら、お金が消えることも増えることもない -- 元の状態に戻す。Feature Flagsによる段階的リリースも、新機能が壊れたら旧バージョンに自動フォールバックするFail Safeです。
Fail Loud -- 静かに壊れるのが一番危ない
問題が発生したら必ず誰かに通知する。Unix哲学のRule 12「修復できないなら、大きな音を立てて壊れろ」の実践です。
// NG: 静かに失敗(誰も気づかない)
async function sendWelcomeEmail(email) {
try {
await emailService.send(email, 'welcome-template');
} catch (error) {
console.log('Email failed, continuing...'); // 握りつぶし
}
}
このコードの恐ろしさは、何百人ものユーザーにウェルカムメールが届いていなくても、誰も気づかない点です。私は実際にこのパターンの障害を半年間見逃した経験があります(発覚したのはユーザーからの問い合わせでした)。
// OK: 段階的に声を上げる
async function sendWelcomeEmail(email) {
try {
await emailService.send(email, 'welcome-template');
metrics.increment('email.sent.success');
} catch (error) {
logger.error('Welcome email failed', { email, error: error.message });
alerting.send('Email service failure', { severity: 'HIGH' });
metrics.increment('email.sent.failure');
throw error;
}
}
Fail Loudは「全部PagerDutyで深夜に叩き起こす」という意味ではありません。影響度に応じてログ→メトリクス→Slack→PagerDutyと段階的にエスカレーションします。
判断基準 -- どの壊れ方を選ぶか
| 状況 | Fail Fast | Fail Safe | Fail Loud |
|---|---|---|---|
| 入力検証 | ✅ 不正データは即拒否 | -- | -- |
| 外部API障害 | ⚠️ 重要機能なら | ✅ 代替手段があれば | ✅ 必ず通知 |
| 権限チェックエラー | -- | ✅ デフォルト拒否 | ✅ セキュリティ警報 |
| データ破損 | ✅ 即座に停止 | ✅ 安全な状態へ移行 | ✅ 緊急アラート |
実際のシステムでは3つを組み合わせます。入口でFail Fast(入力検証)→ 途中でFail Safe(トランザクション管理)→ 出口でFail Loud(監視通知)。1つだけ選ぶのではなく、処理の段階ごとに使い分けるのがポイントです。全部Fail Loudにすると深夜3時にPagerDutyで起こされる回数が激増するので、加減が大事です。
まとめ
壊れ方は技術的制約ではなく、設計判断です。「このコードが失敗したとき、何が起きるべきか?」-- この問いに答えられるなら、あなたのシステムは壊れ方が設計されています。答えられないなら、最悪のタイミングで最悪の壊れ方をする可能性があります。
次にコードを書くとき、try-catchの中身を見てみてください。そこに設計判断はありますか?
📘 この記事の内容をさらに詳しく知りたい方へ
ハーネス・エンジニアリング -- AIを"使う"から"操る"へ -- hooksやlifecycleなど、システムの品質を設計で担保する仕組みを体系的に解説しています