Article

オーバーエンジニアリングという名の投資戦略

高田晃太郎
オーバーエンジニアリングという名の投資戦略

ソフトウェア投資の約40%が技術負債の返済に費やされるという推計・報告が複数存在する¹。Stripeの2018年Developer Coefficientは、開発者の時間の相当部分がバグ修正や再作業(いわゆる「保守」)に割かれ、価値創出に直結しないと指摘する²。さらにDORAの2018年年次レポートでは、いわゆるエリートパフォーマーがリードタイムを1日未満、復旧時間を1時間未満で運用し、変更失敗率を0〜15%に抑える水準が示されている³,⁴。これらは現場の心理的安心の話ではなく、ビジネスの反応速度そのものだ。

ここで意外な問いを置きたい。目の前の要件に対してあえて過剰とも取れる設計を入れることは、本当に無駄なのか。短期の最適化だけが利益を最大化するわけではない。将来の不確実性が高く、変更頻度が高く、失敗のコストが大きい領域では、いわば過剰装備が保険ではなく実行可能性のオプション(後から選べる手段を確保するための権利)になる。過不足のない実装に憧れるほど、現場はしばしば将来の選択肢を失う。

私はオーバーエンジニアリングを「いま不要に見える構造や仕組みへ意図的に資本を配分し、将来の手戻り・障害・ボトルネックを回避する投資」と捉える。重要なのは、信仰ではなく計算でやることだ。過剰を秩序に変えるフレームと、最小のコストで効果を出す実装の姿を、ここで具体的に描いていく。

オーバーエンジニアリングは投資か浪費か

現場で嫌われるのは、必要性が語れない抽象化、誰も読めないフレームワーク化、そして保守を難しくする依存の増殖だ。これらは複雑性を増やし、学習コストを押し上げ、生産性を損なう。一方で、適切に設計された「過剰」は単位変更コスト(1回の変更にかかる時間や手間)の低減、変更失敗率の抑制、インシデントの軽傷化、採用やオンボーディングの高速化に効く。差を分けるのは意図と時期と規模であり、実装の重さではない。

投資として機能する条件は明確だ。不確実性が高いほどオプション価値が上がり、変更頻度が高いほど単位変更コストの削減効果が複利で効く。さらに障害のビジネス損失が大きいほど、観測性(システムの状態を可視化する仕組み)やフォールトトレランス(障害に強い設計)への投資回収は早まる。逆に、寿命が短い実験的プロダクトや一度きりのキャンペーンでは、重厚な層構造は回収できない。

定義の再整理と判断のすり替え

オーバーエンジニアリングとは「必要のない複雑性」ではない。正しくは「現在の価値に対し、将来の価値の仮説が説明されていない複雑性」だ。ここで評価すべきは複雑性それ自体ではなく、説明責任の欠落である。説明できる複雑性は設計であり、説明できない複雑性は装飾に過ぎない。

オプション価値としての設計

金融のリアルオプション(将来の意思決定の自由度に価値を認める考え方)を持ち込むと腹落ちが早い。いま一定のコストを払って拡張や切り替えの権利を買っておくと、環境が変わったときに低コストで価値を取りに行ける。例えばインターフェース分離(使用側と実装側の境界を明確にすること)は、将来のデータソース変更というイベントが発生したときに、実装差し替えの権利を割安で行使できる設計だ。イベントが来なければコストは純損に見えるが、分散環境におけるイベントの到来確率は総じて高い。ここで計算の作法が必要になる。リアルオプション思考はアジリティを高める設計判断として体系化されており⁵、定義上も「将来の経営判断の自由度(実行権)に価値がある」と説明される⁶。

投資判断のフレームと簡易ROIモデル

判断の土台は四つに分解できる。対象ドメインの不確実性、変更頻度、障害コスト、そしてチーム変動だ。顧客要求が変わりやすく、毎週のように仕様が動き、SLO(サービスレベル目標)違反に伴う逸失利益が大きく、半年で人員構成が変わるなら、抽象化・自動化・観測性の投資の価値は跳ね上がる。反対に、要件が固定されていて、変更は四半期に一度、障害許容度が高く、チームも安定しているなら、薄く作って使い捨てる方が賢い。

ここから数式に逃げない程度の簡易モデルを置く。追加設計の初期コストをC、これによって削減される単位変更コストの差分をΔm、期間内の変更回数をn、障害の逓減による期待損失削減をLとする。割引率を無視した単純化では、投資の期待価値はV=Δm×n+L−Cである。ブレイクイーブンはV≥0のときに成立し、n≥(C−L)/Δmが条件になる。例えば、抽象化と観測性の導入に40時間を投じ、変更一回あたりのリリース準備が1時間短縮され、四半期で30回の変更が見込まれ、重大インシデントの早期検知により8時間分の損失Lを抑えられるなら、Δm×n+LはCに現実的に近づく。重要なのは、仮説が外れたときに速やかに撤退できるよう設計を軽量に保つことだ。抽象化は最小限のポート/アダプタ(境界と接続の最小セット)に留め、ドメインの言葉で表現し、生成コードや巨大なベースクラスで囲わない。観測性はサンプリングで十分な精度を確保し、計測のためにアプリ全体を同期化しない。自動化は失敗時の手作業の逃げ道を残して、切り戻し可能な形にする。この三点を守れば、投資の失敗も小さく抑えられる。

現場で効く「最小の過剰」実装パターン

ここからは、Web開発でコスト対効果が高い小さな過剰を、最小限のコードで示す。比較を明確にするため、まずは素朴な実装から置く。

素朴なエンドポイントと限界

import express from "express";
import fetch from "node-fetch";

const app = express();
app.get("/weather", async (req, res) => {
  try {
    const r = await fetch("https://api.example.com/weather");
    const data = await r.json();
    res.json({ temp: data.temp });
  } catch (e) {
    res.status(502).json({ error: "upstream" });
  }
});
app.listen(3000);

動くが、外部API変更や障害、観測、キャッシュ、リトライ、サーキットブレーカ、設定切替に弱い。ここに投資としての薄い層を重ねる。

ポート/アダプタとDIで差し替え可能性を確保

import Fastify from "fastify";
import type { FastifyInstance } from "fastify";

interface WeatherPort { getTemp(): Promise<number>; }
class WeatherApi implements WeatherPort {
  constructor(private readonly base: string, private readonly fetcher: typeof fetch) {}
  async getTemp() { const r = await this.fetcher(`${this.base}/weather`); return (await r.json()).temp; }
}

export function build(app?: FastifyInstance) {
  const f = app ?? Fastify();
  const weather: WeatherPort = new WeatherApi(process.env.API_BASE!, fetch);
  f.get("/weather", async () => ({ temp: await weather.getTemp() }));
  return f;
}

依存をインターフェースで切り出すだけで、モック、将来のデータソース置換、リトライ付加などのオプションを買える。余計なフレームを入れず、関数で組み立て、テストと本番で注入を変える。

観測性は軽量に、トレースとメトリクスを最短導入

import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({ url: process.env.OTLP_URL }),
  instrumentations: [getNodeAutoInstrumentations({
    "@opentelemetry/instrumentation-http": { enabled: true }
  })]
});
sdk.start();

全リクエストを常時トレースする必要はない。サンプリング率を少量から始め、p95遅延(全リクエストの95%が収まる遅延)の変化だけをダッシュボード化する。OpenTelemetryはサンプリングやバッファでオーバーヘッドを抑制できる設計で、適切に設定すれば運用上許容される範囲に収めやすい。

サーキットブレーカとリトライで外部障害を局所化

import CircuitBreaker from "opossum";
import fetch from "node-fetch";

const upstream = new CircuitBreaker(async (url: string) => {
  const r = await fetch(url, { timeout: 1500 });
  if (!r.ok) throw new Error("bad upstream");
  return r.json();
}, { timeout: 2000, errorThresholdPercentage: 50, resetTimeout: 5000 });

export async function getWeather(base: string) {
  return upstream.fire(`${base}/weather`);
}

外部の断続的な障害をアプリ全体に波及させないことが目的であり、ユーザー影響の上限を決める制御だ。失敗時はフォールバックの応答やキャッシュへ逃がす。

キャッシュとSWRで体感性能を底上げ

import Redis from "ioredis";
import fetch from "node-fetch";

const redis = new Redis(process.env.REDIS_URL!);
export async function getTempCached(base: string) {
  const key = "weather:temp";
  const cached = await redis.get(key);
  if (cached) return Number(cached);
  const r = await fetch(`${base}/weather`);
  const temp = (await r.json()).temp;
  await redis.set(key, String(temp), "EX", 60, "NX");
  return temp;
}

まずは最短のTTLキャッシュ(一定時間で失効)から始め、SWR(Stale-While-Revalidate:古い値を返しつつ裏で更新)が必要になったら非同期更新を足す。これだけでも下りAPIに依存するエンドポイントのp95を大きく削れる。

機能フラグで段階的リリースと巻き戻しを確保

import crypto from "node:crypto";

type FlagEval = (userId?: string) => boolean;
const flags: Record<string, FlagEval> = {
  newAlgo: (id) => id ? (parseInt(crypto.createHash("md5").update(id).digest("hex").slice(0,2),16) < 26) : false
};

export function isEnabled(name: string, userId?: string) { return flags[name]?.(userId) ?? false; }

外部SaaSを使わずとも、まずは簡易な百分率ロールアウトから。問題が見えたら即座に旧実装へ戻せることが最も大きな品質保証になる。

ベンチマークと運用コストの実感値

投資の会計は、計測で初めて成立する。例えば、Fastifyで組み立てた素朴版と、上記の最小投資を施した版を用意し、autocannonなどの負荷ツールで30秒・高並列の条件を揃えて測る。比較すべきは、リクエスト/秒、p95・p99遅延、エラー率、CPU/メモリ使用量の変化だ。観測やサーキットブレーカは少なからずオーバーヘッドを生むが、キャッシュや非同期化で取り戻せることが多い。重要なのは、同じテスト条件でビフォー/アフターを測り、差分がSLOの予算内に収まるかを評価することだ。

この評価に対して、障害時の切り離し、段階的リリース、観測による早期検知がもたらす損失回避の便益を並べると、トータルの期待価値はプラスに転じやすい。SLOの観点では、p95やp99の予算をまず宣言し、投資の採否を予算内外で評価するのが実務的だ。たとえばp95を100ミリ秒と置き、観測で10ミリ秒、ブレーカで2ミリ秒のオーバーヘッドが乗るなら、キャッシュや並列度の調整で10〜20ミリ秒を取り戻す計画を同時に設計する。設計は常にトレードオフだが、測りながら配分すれば、過剰は余剰ではなくバッファになる。

最後に、複雑性の借金を増やさないための習慣を添える。抽象化は使われるまで小さく保ち、使われていないコードは定期的に削除する。観測は事後の振り返りで指標を絞り、ダッシュボードを増やさない。自動化は手動の迂回路を残して、障害時に人が介入できるようにする。これらはどれもチームの痛みを減らし、学習速度を保つためのコストであり、長期の複利を生む投資だ。

まとめ:今日の一手が、明日の選択肢を増やす

オーバーエンジニアリングは呪いの言葉ではない。説明できる過剰は、変化に強いプロダクトを作るための資産になる。あなたのシステムにとって、どの領域が不確実で、どの変更が頻発し、どの障害が高くつくのかを短く言語化してみてほしい。その三点に対してだけ、最小の層を重ね、計測し、回収計画を明示すれば、過剰は浪費ではなく意図ある投資に変わる。

次のスプリントでやるべきことは難しくない。重要なエンドポイントをひとつ選び、薄いポート/アダプタ、軽量のトレース、簡易キャッシュ、そして機能フラグの四点セットを入れて計測する。数字が語る物語をチームで共有し、投資の続行か撤退を議論しよう。今日の一手が、明日の選択肢を確実に増やす。

参考文献

  1. Vaultinum. Technical Debt and ROI. https://vaultinum.com/blog/technical-debt-and-roi#:~:text=Grown,of%20control%20if%20not%20managed
  2. ADTmag. Stripe Developer Coefficient: 2018 survey coverage. https://adtmag.com/articles/2018/09/10/developer-survey.aspx#:~:text=One%20major%20highlight%20of%20the,in%20opportunity%20cost%20lost%20annually
  3. DORA. Accelerate: State of DevOps Report 2018. https://dora.dev/research/2018/dora-report/#:~:text=,likely%20to%20be%20elite%20performers
  4. The Register. DORA DevOps report coverage (change failure rate 0–15%). https://www.theregister.com/2021/09/23/dora_devops_report#:~:text=DORA%20categorizes%20these%20folks%20into,cent%20failure%20rate%20on%20changes
  5. InfoQ. Real Options — Dealing with Uncertainty to Enhance Agility. https://www.infoq.com/articles/real-options-enhance-agility/#:~:text=To%20use%20a%20familiar%20phrase%2C,more%20formal%20definition%20will%20follow
  6. Investopedia. Real Option. https://www.investopedia.com/terms/r/realoption.asp#:~:text=A%20real%20option%20gives%20management,instead%20of%20a%C2%A0financial%20instrument