その設計判断、なんで選んだか3秒で言える?🤔

その設計判断、なんで選んだか3秒で言える?🤔

これ絶対あるじゃん??これ絶対あるじゃん??

  • 「なんでここキャッシュ使ってるんだっけ?」って自分のコードを30分読んだ
  • 「データの正しさより落ちないこと優先で〜」って口で言えるのに、コードにそれが1行も書いてない
  • PRレビューで「なんでRedis使ったんですか?」って聞かれて「まあ速いので」しか言えなかった
  • 新しいメンバーが「前の設計の意図ってなんですか?」って聞いてきて、チーム全員が無言になった

これがUndocumented Design Decision(暗黙の設計判断)、以降「口伝コード」って呼ぶね、が原因なんだよね。


てかシンプルに言うとねてかシンプルに言うとね

設計判断を言葉にしてない = 「なぜ選んだか」が消えた状態でコードが動き続けてるっつーこと。

「それの何がまずいの?ってなる気持ち、わかる!」動いてるし、テストも通るし、デプロイもできてる。でもさ、たとえば「データの正しさ」と「落ちないこと」のどっちを取るかを決めたのに——それを誰も知らないまま時間が経ったら?

次に触った人が、同じ悩みをゼロから考えることになる。 あるいは気づかずにそのさじ加減をぶっ壊す。


ちょっと待って、先に全然関係ない話していい?🏠ちょっと待って、先に全然関係ない話していい?🏠

都内のタクシー会社「丸幸交通」に、鈴木さんっていうベテランドライバーがいたんだけどさ。20年やってて、特にラッシュ時の都心ルートが得意。「渋滞を避けて早く着く」って評判が社内にあったの。

問題は、鈴木さんが「なぜその道を選ぶか」を一度も言葉にしたことがなかったこと。ここがヤバいんだけど。

「なんとなく体で覚えてる」「経験だから説明できない」

それでも鈴木さんがいる間は何も起きなかったんだよね。

で、鈴木さんが辞めた月。ここから全部崩れるの。

後輩の宮本さんが初めてラッシュ時の都心ルートを任された。

「宮本さん、今日はどの道を?」と同乗した先輩の田村さんが聞いた。

「環状線が速そうかなって思って」

「鈴木さんは絶対その道行かなかったんだけどな」

見事に渋滞にはまった。お客さんに謝りながら、乗車時間が40分増えた。

「鈴木さんならどうしてたんだろう。なんでダメだったんだろう」

翌日も、翌々日も、同じことが起きた。新しく入った4人のドライバーが全員、同じポイントで同じ判断を間違えた。

会社のアプリ評価は1ヶ月で4.3から3.7に落ちたんだよね。顧客からのクレームは週30件を超えた。橋本部長は「なんで鈴木さんがいたときは問題なかったんだ」と毎朝会議で言い続けたらしい。えぐくない?

「鈴木さんの判断は鈴木さんの頭の中にしかなかった。ルートを選んだ理由は、鈴木さんと一緒に退職した」

これが本番で起きてるやつが、「設計判断を言葉にしないまま作っちゃう」ってこと。


じゃあコードで見てみよっか👀じゃあコードで見てみよっか👀

鈴木さんポジションのエンジニアが書いたコード、よくあるやつ。

import json
import redis

CACHE_TTL = 60  # なんとなく60秒

class OrderRepository:
    def __init__(self):
        self.cache = redis.Redis()

    def get_order(self, order_id: str) -> dict:
        # キャッシュから取得を試みる
        cached = self.cache.get(f"order:{order_id}")
        if cached:
            return json.loads(cached)

        # なんとなくレプリカを使う
        order = self.read_replica.query(
            "SELECT * FROM orders WHERE id = %s", order_id
        )
        self.cache.setex(f"order:{order_id}", CACHE_TTL, json.dumps(order))
        return order

    def update_order(self, order_id: str, status: str):
        # プライマリDBを更新
        self.primary_db.execute(
            "UPDATE orders SET status = %s WHERE id = %s",
            (status, order_id)
        )
        # キャッシュのinvalidateは……なし

この設計に込められた判断:

  • なぜTTL 60秒?→ 誰も知らない
  • なぜレプリカから読む?→ 誰も知らない
  • なぜ更新時にキャッシュをinvalidateしない?→ 誰も知らない

半年後、新しいメンバーが update_order の直後に get_order を呼ぶ処理を追加した。古いステータスが返り続けた。本番で注文データの不整合が発生した。深夜2時にSlack通知が来た。

原因を追うのに2日かかったのは、「なぜこの設計にしたか」がコードのどこにも書いてなかったから。わたしも昔これやったから言えるんだけど、「コードは正しいのに結果がおかしい」状態の調査は本当に時間が溶ける。


やばいのまだあんだけど、バグに気づけない問題😭やばいのまだあんだけど、バグに気づけない問題😭

「壊れやすい」だけじゃなくて、**「壊れてても何が正しいか分からない」**状態になるのが本当にえぐい。

鈴木さんが「なんで環状線を避けたか」を言葉にしてなかったせいで、宮本さんは「環状線がダメなのか、時間帯がダメなのか、曜日がダメなのか」すら判断できなかった。

コードも同じ。

def test_get_order_after_update():
    repo = OrderRepository()
    repo.update_order("order-123", "shipped")
    result = repo.get_order("order-123")

    # "shipped" を返すべき?
    # それとも、キャッシュから古いデータを返すのが「正しい」動作?
    # → 設計判断が言語化されていないので、テストの期待値が書けない

    assert result["status"] == ???  # 正解が存在しない

「落ちないことを優先するためにキャッシュを使い、データの正しさは60秒以内しか保証しない」って判断がはっきり書いてあれば、assert result["status"] == "processing"(古いデータが正しい)と書ける。

でも言葉にしてないと、バグかどうかすら判断できない。設計判断を言葉にしないのは、壊れやすいだけじゃなく、バグを隠す設計でもある。


ちゃんと書き残してたチームの話もするね〜✨ちゃんと書き残してたチームの話もするね〜✨

鈴木さんの後輩が就職した別の会社、林さんのチームの話。

林さんの会社には「ルート選択ログ」があった。ドライバーが「今日は環状線を避けた。理由:水曜18時台は渋滞確率90%。代替:首都高経由、料金+200円だが20分短縮」って30秒でアプリに入力する仕組み。

林さんが退職しても、後輩が困ることはなかった。ログを読めば「なぜ」が全部わかるから。クレームゼロ。定時退社。橋本部長みたいな人も現れなかった。

コードでも同じことをやる。

# ─────────────────────────────────────────
# 設計判断メモ(2024-03-01 山田)
#
# トレードオフ:一貫性(Consistency) < 可用性(Availability) を選択
#
# 理由:
#   - 注文ステータスの1分程度のラグはビジネス上許容されている(PM確認済)
#   - ピーク時のDB負荷軽減が最優先(SLA: 99.9% uptime)
#
# 代償(必ず守ること):
#   - 更新系APIは必ずキャッシュをinvalidateする責任を持つ
#   - TTLを5分以上にしない(整合性SLAが崩れる)
#
# 再考タイミング:決済処理が同一システムに入ったとき(一貫性優先に切替が必要)
# ─────────────────────────────────────────

CACHE_TTL = 60  # 1分(上記メモ参照)

class OrderRepository:
    def get_order(self, order_id: str) -> dict:
        cached = self.cache.get(f"order:{order_id}")
        if cached:
            return json.loads(cached)

        # 設計判断メモより:可用性優先のためレプリカから読み取り
        order = self.read_replica.query(
            "SELECT * FROM orders WHERE id = %s", order_id
        )
        self.cache.setex(f"order:{order_id}", CACHE_TTL, json.dumps(order))
        return order

    def update_order(self, order_id: str, status: str):
        self.primary_db.execute(
            "UPDATE orders SET status = %s WHERE id = %s",
            (status, order_id)
        )
        # 設計判断メモの「代償」に従いinvalidate(省略厳禁)
        self.cache.delete(f"order:{order_id}")

新しいメンバーが来ても「なぜこうなっているか」がコードと一緒にある。「いつ見直すか」まで書いてあるから、仕様変更が来たときに判断できる。深夜Slack通知なし。


ちゃんとした名前もあるから一応言うね📚ちゃんとした名前もあるから一応言うね📚

Architecture Decision Record(以降ADRって呼ぶね)とは、設計で何を選んで、何を捨てたかを残しておくっつーこと。

広めたのはMichael Nygardって人で、2011年のこと。

設計って常に「何かを選んで何かを捨てる」の連続なんだよね。ADRはその「何を選んで何を捨てたか」を書き残すやり方。今はGitHubに docs/adr/ ってフォルダ作って残すのが定番ね。

難しそうに聞こえるけど、要は「なぜこっちを選んで、何をあきらめて、いつ考え直すか」って聞き続けるだけなんだよね。


これだけ覚えて帰って👌これだけ覚えて帰って👌

合言葉はこれ。nkyo(なんでか書けよ)

コードを書くとき・レビューするとき、この問いを使って。

「この設計判断が変わるとしたら、どんな出来事が起きたとき?」

たとえばさっきの「TTL 60秒・落ちないこと優先」の判断。

  • 出来事①:「決済機能が同じシステムに入った」→ 整合性の厳密な保証が必要になる。キャッシュ戦略を根本から変える
  • 出来事②:「DAUが10倍になって、DBへの読み取りが限界に」→ TTLを伸ばして「落ちないこと」をさらに優先する
  • 出来事③:「SLAに『ステータスのリアルタイム反映』が明記された」→ キャッシュをやめる選択肢が浮上する

出来事が3種類ある = この判断は「3人の鈴木さん分の理由」で変わりうるってこと。

それだけの重みがある判断を、なぜ選んだか誰も知らない状態にしてたら、変えるタイミングすら分からなくなる


まとめるとこういうことねまとめるとこういうことね

観点言語化なし(アンチパターン)言語化あり(正しい設計)
設計意図の継承担当者が変わると意図が消えるコメント・ADRに残り続ける
テストの書きやすさ期待値の「正解」が定義できないトレードオフが前提なので期待値が書ける
機能拡張時無意識にトレードオフを破壊する判断の前提を踏まえて拡張できる
障害調査設計意図の追跡に数日かかる数分で「なぜこうしたか」が分かる
設計変更のタイミング「変えていいか」の判断基準がないどんな出来事が起きたら変えるかが明示されている

コードに5行追加するだけで、半年後の自分とチームを救えるっつーこと。鈴木さんみたいに知識を頭の中だけに持ったまま去らないで🫶


沼りたい人はこっちも読んで📖沼りたい人はこっちも読んで📖

権威ある書籍

  • Michael Nygard『Release It! Design and Deploy Production-Ready Software』(2018年 第2版、Pragmatic Bookshelf)— サーキットブレーカー・バルクヘッドなど本番システムの安定性パターンを扱った定番書
  • Neal Ford, Rebecca Parsons, Patrick Kua『Building Evolutionary Architectures』(2017年、O'Reilly)— 変化に強いアーキテクチャ設計とFitness Functionを提唱した書籍。設計上のトレードオフを継続的に評価・検証する考え方を扱っている
  • Eric Brewer「Towards Robust Distributed Systems」(2000年、PODC基調講演)— 一貫性・可用性・分断耐性のトレードオフを最初に提唱した原著

一次資料

無料で読める入門記事


Dev-Here「ギャルでも分かる設計の勘ドコロ!」シリーズ 第5回