ハッピーパスを設計する。ホワイトボードにボックスを描く。ユーザー → API → データベース。きれいな矢印。シンプルな流れ。リリース。
6ヶ月後、システムは47個のtry/catchブロック、3つの異なるログフォーマット、すべてのコントローラーに散らばった認証チェック、そして誰も上限を設定しなかったため永遠にリトライするリトライロジックで成り立っている。
横断的関心事へようこそ — システムのあらゆる部分に触れるが、どこにも属さないもの。
アプリケーション全体を横断する責任で、1つのモジュールの中に収まらないもの:
これらは機能ではない。誰もロードマップに「ロギングを追加」とは書かない。でもシステムが本番環境で生き残るかどうかを決める。
15行の横断的ロジック。実際のビジネスロジックは2行。そしてこのパターンがすべてのエンドポイントにコピペされ、それぞれ微妙に違う。
関心事をビジネスロジックから引き出す。ミドルウェアレイヤーとしてスタックする。
各関心事は1回定義し、1回テストし、一貫して適用される。
サービスではデコレータやアスペクトを使う:
関数の本体は純粋なビジネスロジック。それ以外はすべてその周りに合成される。
マイクロサービスの世界では、横断的関心事をエッジに押し出す:
APIゲートウェイが認証、レート制限、ロギング、トレーシングが設定される唯一の場所になる。個々のサービスはこれらの存在すら知らない。
高度なセットアップでは、IstioやLinkerdのようなサービスメッシュがネットワークレベルで横断的関心事を処理する:
アプリケーションコードはそのどれも知る必要がない。
本番に行く前に、すべてのシステムがこれらに答える必要がある:
| 関心事 | 問い | 一般的な解決策 | |--------|------|--------------| | 認証 | アイデンティティをどう検証する? | JWT、OAuth2、APIキー | | ロギング | フォーマットは?どこに送る? | 構造化JSON → ELK/Datadog | | トレーシング | サービス間でリクエストを追跡できる? | OpenTelemetry、Jaeger | | リトライ | リトライポリシーは? | ジッター付き指数バックオフ | | レート制限 | 不正利用をどう防ぐ? | ゲートウェイでのトークンバケット | | サーキットブレーカー | 下流が死んだらどうなる? | N回のエラー後にフェイルファスト | | バリデーション | 入力をどこでバリデーションする? | エッジバリデーション + スキーマ強制 | | エラーハンドリング | 失敗時にクライアントには何が見える? | 一貫したエラーエンベロープ |
横断的関心事は:
認証チェック、ロギング、リトライロジックがビジネス関数の中にあるのを見た瞬間、関心事が散らばっている。システムは今日は動く。来年は生き残れない。
午前3時に重要なのは、退屈なインフラの決断だ。
— blanho
APIゲートウェイは外部のカオスを処理する。サービスメッシュは内部のカオスを処理する。
CRUDは履歴を上書きする。イベントソーシングはすべてを記憶する。それが重要な場面とオーバーキルな場面。
冗長パイプライン、インテリジェントなセグメント選択、カスタムストレージレイヤー — Netflix Live Originアーキテクチャの内側。
@authenticate
@rate_limit(max_calls=100, period=60)
@retry(max_attempts=3, backoff="exponential")
@log_execution
@trace
def get_user(user_id: str) -> User:
return db.find_user(user_id)# APIゲートウェイがすべてのサービスのためにこれらを処理:
gateway:
authentication: jwt-validation
rate_limiting: 1000/min per client
logging: structured-json
tracing: opentelemetry
circuit_breaker: 50% error threshold
# サービスはビジネスロジックだけを処理
user-service:
GET /users/{id}: "ユーザーを取得するだけ。それだけ。"// すべてのコントローラーがこうなる
async function getUser(req: Request) {
// 認証チェック(別のコントローラーからコピペ)
const token = req.headers.authorization;
if (!token) return res.status(401).json({ error: "Unauthorized" });
const user = verifyToken(token);
if (!user) return res.status(403).json({ error: "Forbidden" });
// ロギング(他のコントローラーとフォーマットが違う)
console.log(`[${new Date().toISOString()}] getUser called by ${user.id}`);
try {
// レート制限チェック(手作り、たぶん間違ってる)
const count = await redis.incr(`rate:${user.id}`);
if (count > 100) return res.status(429).json({ error: "Too many requests" });
const result = await db.findUser(req.params.id);
// さらにロギング(上のと一貫性がない)
logger.info("User fetched", { userId: req.params.id });
return res.json(result);
} catch (err) {
// エラーハンドリング(他のすべてのコントローラーと違う)
console.error("Error:", err);
return res.status(500).json({ error: "Something went wrong" });
}
}// 各関心事は個別のテスト可能なミドルウェア
const pipeline = [
rateLimiter({ maxRequests: 100, window: "1m" }),
authenticate(),
authorize("users:read"),
requestLogger(),
errorHandler(),
];
// ビジネスロジックはビジネスロジックだけ
async function getUser(req: Request) {
const user = await db.findUser(req.params.id);
return res.json(user);
}
app.get("/users/:id", ...pipeline, getUser);