【ITニュース解説】Stale By Design
2026年09月09日に「Dev.to」が公開したITニュース「Stale By Design」について初心者にもわかりやすく解説しています。
ITニュース概要
アプリ内のキャッシュデータが古くなる問題を解決する「Stale By Design」という設計手法を解説。関連のない機能がデータを使う側を知らずにデータを「古い」とマークでき、画面表示などでデータが必要になった時だけ再読み込みする。これにより、無駄なデータ取得を防ぎ、常に最新の情報を表示できる。
ITニュース解説
私たちが日々利用するスマートフォンアプリやウェブサイトは、表示される情報を高速に、そして正確に届けるために様々な工夫をしている。その一つが「キャッシュ」と呼ばれる技術だ。これは、一度取得したデータを一時的に保存しておき、次に同じデータが必要になったときに、わざわざサーバーから再取得せずに保存しておいたデータを利用することで、アプリの動作を速くし、サーバーへの負担を減らす。しかし、このキャッシュには一つの大きな問題がある。それは、保存されたデータが古くなってしまう「データの鮮度」の問題だ。
例えば、あなたの使っている社内掲示板アプリが、チームのメンバーリストをキャッシュしていると想像してみよう。アプリを起動したとき、メンバーリストは一度だけサーバーから取得され、アプリ内に保存される。その後、あなたはメンバーリストを何度も参照するが、そのたびにサーバーに問い合わせるわけではない。保存しておいたキャッシュを利用するため、非常にスムーズにリストが表示される。ここまでは非常に便利だ。
しかし、もし誰かがチームメンバーの名前を変更したり、別のチームに移動したりしたらどうなるだろう。サーバー上の情報はもちろん更新されるが、あなたのアプリ内に保存されているキャッシュは、その変更を知らない。結果として、あなたのアプリには古いメンバーリストが表示され続け、誤った情報に基づいて行動してしまう可能性がある。これはユーザーにとって大きな混乱を招く。この古い情報を表示し続ける状態を「ステイル(stale)」と呼ぶ。
この問題を解決しようとすると、すぐに二つの選択肢が頭に浮かぶが、どちらも完璧ではない。一つ目の方法は、「可能性のあるすべての変更のたびに、関係するデータをすべて再取得する」というものだ。メンバーの名前が変更されるたびに、掲示板リスト、プロジェクトリスト、権限リストなど、関連するすべてのデータをサーバーから再取得する。これは確実だが、ユーザーが本当に必要としているかどうかにかかわらず、大量の無駄な通信が発生し、アプリの動作が重くなる原因となる。
二つ目の方法は、「データの変更を引き起こす処理が行われた場所に、手動でデータの再取得処理を追加する」というものだ。例えば、メンバーの名前を変更する処理の後に「掲示板リストを再取得する」というコードを追加し、チームを切り替える処理の後に「掲示板リストを再取得する」というコードを追加する。この方法は、必要なときにだけ再取得するため無駄は少ないように見える。しかし、時間の経過とともに、データの変更を引き起こす箇所は増えていく。新しい機能が追加され、新たな方法でメンバー情報が変更されるようになったとき、古いキャッシュを更新する処理を追加し忘れる可能性が高まる。すると、結局はまた古い情報が表示され続けてしまうことになる。このような手動での管理は、バグの温床となりやすく、システムの保守を非常に困難にする。
そこで提案されるのが「Stale By Design」という考え方だ。このアイデアは、データを「古いとマークすること」と「実際にデータを再取得すること」という二つの異なる仕事を、きれいに分離することを目指す。
まず、「データが古いとマークする」という仕事は、非常に軽い処理として扱う。アプリのどこか、例えばメンバーの名前が変更されたり、チームが移動したりするような出来事が起きたら、関係するデータが「古い」状態になったと通知する。このとき、通知する側は「誰がこの古いデータを読んでいるのか」を知る必要はない。ただ、「古い状態になったよ」と伝えるだけで良い。
次に、「実際にデータを再取得する」という仕事は、コストがかかる処理なので、本当にそのデータが必要とされている時だけ行う。つまり、画面に古いデータが表示されていて、ユーザーがそのデータを必要としている、もしくは新しいデータを表示しようとしている時だけ、サーバーへ問い合わせて最新のデータを取得するのだ。
この二つの仕事を分離するために、「status(状態)」という単一のフィールドをデータに持たせる。この「status」フィールドは、データが現在どのような状態にあるかを示すためのものだ。具体的な状態としては、「idle(待機中)」「stale(古い)」「loading(読み込み中)」「fresh(最新)」「error(エラー)」の五つが考えられる。これにより、データが二つの異なる状態を同時に持つなどして矛盾が発生するのを防ぐことができる。
実際にこれをコードで実現する仕組みを見てみよう。
データ構造には、例えば掲示板リストのデータ本体と、この「status」フィールドを持たせる。ReloadStatusという型でこれらの状態を定義し、掲示板データのstatusプロパティが常にこれらのいずれかの状態を持つようにする。
データを「stale」とマークするプロセスは次のようになる。アプリケーション内で「チームを切り替えた」や「メンバーの名前変更に成功した」といった、データに影響を与える可能性のあるアクションが発生した際に、そのアクションを受け取って「掲示板リストをstaleにする」という専用のアクションを発火させる。このとき、「チームを切り替える」アクション自体は、掲示板リストが存在することすら知る必要がない。ただ自分の役割を果たすだけで、その結果として掲示板リストが古くなる可能性があれば、それを処理する別の仕組みが反応する。
ここで注意すべき点として、もしすでに掲示板リストのデータが「loading(読み込み中)」状態にあった場合、それをすぐに「stale」に戻してしまうと、せっかく始まった読み込みが無駄になってしまう可能性がある。そのため、「loading」中に「stale」アクションが発火しても、状態は「loading」のまま維持し、読み込みが完了するのを待つようにする。
次に、データを「再読み込み」するプロセスだ。「掲示板を読み込む」というアクションが発火すると、データは「loading」状態になり、実際にサーバーへデータを問い合わせる処理が始まる。この処理が成功すれば、新しいデータがアプリに格納され、statusは「fresh(最新)」になる。もし失敗すれば、statusは「error(エラー)」となる。freshになるのは、実際にデータの取得に成功した場合のみである。
最も重要な部分が、この再読み込みを「いつ」「どのように」トリガーするかだ。アプリの画面上で掲示板リストを表示するコンポーネントは、それぞれが自分の表示責任を持っている。しかし、もしデータが「stale」になったときに、そのデータを表示しているすべてのコンポーネントが同時に再読み込みを始めたら、無駄なリクエストが何重にも送られてしまう。これを避けるために、複数のコンポーネントが同じデータを必要としていても、データが「stale」になったら、一度だけ「掲示板を読み込む」アクションが発火するように制御する。
この制御は、コンポーネントが初期化されるタイミング(画面に表示され始めるタイミング)で特定のメソッドを呼び出すことで行われる。このメソッドは、データのstatusの変化を監視し、「stale」状態になったことを検知したら、一度だけ再読み込みアクションを発火させる。このとき、もし複数のコンポーネントが同時にこのメソッドを呼び出しても、裏側では一つの監視ストリームが共有されるようにする。これにより、何回「stale」と通知されても、実際に「loading」処理が開始されるのは一度きりとなる。
また、アプリを最初に起動したときや、データ読み込みが前回エラーで終わっていた場合など、statusが「idle」や「error」の状態である場合、ユーザーは空のリストを見せられたり、古いエラーメッセージを見せられたりする可能性がある。これを避けるため、コンポーネントが画面に表示される瞬間に、statusが「idle」か「error」であれば、一度だけ強制的に再読み込みを行うようにする。これにより、ページを開いたときに常に最新、もしくは取得試行中の状態を保つことができる。
このような仕組みでは、外部から直接データやアクションを操作されると意図しない挙動につながる可能性がある。そのため、外部に公開するインターフェースは最小限に絞る。例えば、実際の掲示板リストデータ、現在のstatus、そして再読み込みをトリガーするメソッドのみを外部に公開し、内部のアクションやセレクタは隠蔽する。これにより、このキャッシュ機構の誤用を防ぎ、安全性を高める。
このような複雑な仕組みを導入することは、一つのキャッシュリストのためだけなら、少し大げさに感じるかもしれない。しかし、このアプローチが真価を発揮するのは、複数のキャッシュされたデータが、同じ種類の変更に反応する必要がある場合だ。例えば、メンバーの名前が変更されたとき、掲示板リストだけでなく、プロジェクトリストや、そのメンバーの持つ権限リストなど、複数のデータが「古い」状態になる可能性がある。この「Stale By Design」の仕組みを使えば、メンバー名変更の処理は、それらの個々のリストの存在を知ることなく、「関係するすべてのデータを古いとマークする」という単一の意図を伝えるだけで済む。それぞれのキャッシュリストは、自分自身が古いとマークされたら、必要に応じて再読み込みする、という自身の役割に集中できるのだ。これは、システム全体の結合度を低くし、保守性を大きく向上させる効果がある。
もしあなたがAngularのようなフレームワークを使っているなら、TanStack Queryのようなツールは、invalidateQueriesという機能を提供しており、より少ないコードで同様のキャッシュ無効化と再フェッチの仕組みを構築できる。しかし、もしあなたがすでにNgRxのような強力な状態管理ライブラリを深く利用しており、システムのデータ層全体を変更するのが現実的ではないような状況であれば、この記事で紹介したような「Stale By Design」の考え方を取り入れることは、既存のシステムを大きく変えずに、キャッシュの鮮度問題を解決するための非常に有効な手段となるだろう。これは、現実のシステム開発で直面する「古いキャッシュによるバグ」から着想を得て考案された、実践的な解決策なのである。