世界を動かす技術を、日本語で。

良いシステム設計について私が知っているすべてのこと

2025年8月16日原文(seangoedecke.com)

概要

  • システム設計に関する誤ったアドバイスの多さ
  • 良いシステム設計とは「目立たない安定性」である点の強調
  • 状態管理(State)と無状態(Stateless)の重要性
  • データベース設計とパフォーマンス最適化の基本
  • バックグラウンドジョブやキャッシュの使い方に関する実践的指針

システム設計の本質と誤解

  • LinkedInTwitter で見かける「キューを使えば全て解決」や「データベースにBooleanを保存するな」といった極端なアドバイスの危険性
  • Designing Data-Intensive Applications などの名著も、現場の多くの問題には適用しづらいケースが多い点
  • ソフトウェア設計が「コードの組み立て」なら、 システム設計 は「サービスの組み立て」であるという定義
    • ソフトウェア設計のプリミティブ:変数、関数、クラスなど
    • システム設計のプリミティブ:アプリサーバ、データベース、キャッシュ、キュー、イベントバス、プロキシなど
  • 良いシステム設計の特徴は「何も起きない」ことで、派手さよりも安定運用が重要
  • 複雑なシステムは、しばしば設計ミスや過剰設計の証拠

状態管理と無状態設計

  • ソフトウェア設計の難所は 状態管理
  • 情報を保存しないサービスは「 Stateless(無状態)
    • 例:GitHubのPDF→HTML変換APIなど
  • データベースへ書き込むサービスは「 Stateful(有状態)
  • システム内の 有状態コンポーネント は最小限に抑えるべき
    • 有状態のものは障害時の自動修復が難しく、手動対応が必要になる
  • 状態を扱うサービスを1つに集約し、他はAPI経由やイベント駆動で処理分担を推奨
  • 読み取りも一元化できれば理想だが、パフォーマンス次第で柔軟に対応

データベース設計と運用の基本

  • スキーマ設計 は柔軟性と可読性のバランスが重要
    • 何でもJSONやKey-Valueで保存するとアプリ側が複雑化しやすい
    • テーブル設計は「人間が見て分かる」ことを重視
  • インデックス はよく使うクエリに合わせて設計
    • インデックスの付けすぎは書き込みコスト増加の原因
  • データベースアクセスが ボトルネック になりやすい
    • JOINでデータ取得を効率化、ORMの無駄なクエリ発行に注意
    • 複雑すぎるクエリは場合によって分割も選択肢
  • リードレプリカ 活用で書き込みノードの負荷分散
    • レプリケーション遅延への配慮も必要
  • クエリやトランザクションのスパイク時は スロットリング の検討

バックグラウンドジョブと遅延処理

  • ユーザー体験に直結する処理は 高速化、それ以外は バックグラウンドジョブ に分離
  • バックグラウンドジョブの基本構成
    • キュー(例:Redis)
    • ジョブランナーサービス
  • 定期実行や将来実行が必要な場合は DBテーブル で管理、スケジューラで処理

キャッシュ設計の注意点

  • キャッシュは「高コストな処理の再利用」が目的
    • 例:価格APIの頻繁な呼び出しを5分単位でキャッシュ
  • キャッシュはアプリ内メモリや Redis / Memcached など外部KVSを活用
  • キャッシュの乱用は 状態管理の複雑化不整合 の原因
    • インデックス追加等、まずは根本的な高速化を優先
  • 大規模・長期キャッシュが必要な場合は S3Azure Blob Storage とスケジュールジョブの組み合わせも有効

この内容は、実践的なシステム設計の原則と注意点をまとめたものです。安定性・シンプルさ・状態管理の徹底が、良いシステム設計の鍵となります。

Hackerたちの意見

ロギングとメトリクスに関するアドバイスは良かった。状態やプッシュ/プルについて頷いていたけど、この部分は特に気を引かれた。こんなに明確に説明されているのは初めて見たから。

ロギングの部分はその通りだね。「ああ、これをログに残しておけばよかった」と思うことが何度もあったし、問題やインシデントに直面したときに結局ログを導入することになる。

そうそう。みんな少しの時間を使ってロギングやメトリクスを整備すべきだよ。テストみたいなもので、0から1のテストを作るのは心理的に難しいけど、1から1000になると「これなしでどうやって生きてたんだろう」ってなる。Grafanaにはそこそこ使える無料プランがあるし、自分でホスティングすることもできるよ。

タイムスタンプを保存するべきで、タイムスタンプの存在を真実として扱うべきだよ。たまにこれを実践するけど、いつもではないかな。データベースのスキーマをすぐに読めるように保つことにも価値があると思う。良いパターンに対するアドバイスとしてはちょっと否定的すぎる気がする。is_on => true on_at => 1023030 なるほど、理解できる。is_a_bear => true a_bear_at => 12312231231 これはあまり良くないね。ほとんどのクマは、クマでない時期があったりしないから。

その発言をそのまま受け取ると、基本的にデータベースにブール値を保存するのは悪臭だと言えるね。彼の言う通りだと思う。ただ、これが広く良い原則かどうかは疑問だし、on_atの場合でもそう。もしこういうことを気にするなら、適切に監査テーブルに保存すべきだよ。ブール値をタイムスタンプに切り替えるのは、実際にはあまり役に立たない奇妙な怠け者のハックだと思う。なぜなら、そんな風に追跡されるのはランダムなデータのサブセットだけだから。ブールデータ型が更新時間を追跡するのに重要かどうかを決める要因ではないし。これが提案される主な理由は、たぶん「無料」だからだと思う。ブール値にタイムスタンプを忍ばせることができるし、偶然にいくらかの手間を省いたんだろうけど、解決しようとしている問題のセットに対する完全な解決策ではないと思う。ソフトデリートにも同じ疑念を抱いている。実際には役に立たないと確信しているし、適切な監査を避けるための精神的に怠惰な解決策に過ぎない。確実に「元に戻す」ことはできないし、更新履歴を解決するわけでもないから、実際に守っているのは偶発的な一括削除を即座にキャッチすることだけ?それがバックアップの半分の目的だよね。

その状況では、Bearと他のカテゴリを含むenum値を持つことができると思うよ。

でも、なぜブール値を特別扱いして、これを持たない整数に対してタイムスタンプを保持するの?: isDarkTheme: {timestamped} paginationItems: 50 ダークテーマがいつ有効になったかはわかるけど、ページネーションが50に設定されたのはわからないし、ダークテーマが無効になった時もわからない。貧乏人の変更履歴みたいだ。使い道はあるかもしれないけど、正直言って思いつかないな。

こういう一般的なアドバイスは全然役に立たないし、何百万ものアスタリスクが必要だよ。良いシステム設計は、目の前の問題に最適なシステムを設計することなんだ。

ほとんどのケースでブール値は悪いものだと思う。ブール値の代わりにタイムスタンプや整数フィールド(後で拡張可能)を使うべきだ。is_aの場合、ほとんど常にタイプや種類の方が良い。たとえ最初はクマだけでも、クマだけの状態はあまりないし、ステータスフィールドも(オンかオフだけではなく)通常はサスペンド、削除、スリープなどに広がるからね。だから、一般的にはブール値は避けるべきだと思う。相互排他的な状態(ライブ、削除、サスペンドなど)をカバーする時に、ブール値は増殖して複雑さを増すことが多いから。is_visible、is_deleted、is_suspendedが同じテーブルにあって(ステータスなしで)、その結果のコードやクエリは見栄えが良くない。代わりにタイムスタンプではなく整数を使うべきだと思う。

ブール値はサイズが小さいから、特定のワークロードには重要な考慮事項だよ。例えば、関連するタイムスタンプを気にしない分析クエリのセットに対して、大量のデータを事前に集計している場合がある。その場合、小さいデータ型の方がストレージとクエリ実行の両方で効率的なんだ。さらに、ブール値を保存するのが論理的な状況もあるよ。例えば、ブール値が結果を示す場合:process_executed_at タイムスタンプは null でない、process_succeeded ブール値は null でない。

データベースをクエリするときは、データベースをクエリするべきだよ。自分でやるよりも、データベースに仕事をさせる方がほぼ常に効率的だよ。例えば、複数のテーブルからデータが必要な場合は、別々のクエリを作るんじゃなくて、JOINして一緒に取得するべき。そうそう!アプリケーションコードでJOINは絶対にやっちゃダメ!でも、ビューも使ってね!(できればストアドプロシージャも) ビューは基盤となるデータの抽象化で、機能的な性質を持っていて、将来的にランダムな理由で壊れる可能性も低いし、うまくやれば基盤のSQLコードは驚くほど読みやすくて理解しやすいよ。

ビューは、チェックインできるときには理にかなっているよ。DBマイグレーションはその不変性のために良い方法ではないし。コードベースが採用するエコシステムによっては、良いORMを使ってJOINする方が良い選択かもしれないね。

Hacker Newsで議論の続きを見る