技術力とシステム設計
技術力とシステム設計
Section titled “技術力とシステム設計”シニアエンジニアの設計思想
Section titled “シニアエンジニアの設計思想”ジュニアが「点(コード)」で書くのに対し、シニアは「線(時系列・状態)」と「面(システム全体・非機能要件)」で設計します。特に「冪等性」や「可観測性」をデフォルトの設計に組み込む姿勢は、運用フェーズでの「地獄」を未然に防ぐシニアの必須スキルです。
信頼性を構造で担保する
Section titled “信頼性を構造で担保する”シニアエンジニアの設計は、ハッピーパス(正常系)よりも「アンハッピーパス(異常系)」に多くのリソースを割きます。これは、本番環境で発生する問題の大半が想定外の状況から生まれるためです。
システム単位での思考
Section titled “システム単位での思考”機能単位ではなくシステム単位で考える
Section titled “機能単位ではなくシステム単位で考える”ジュニア・ミドルの思考:
// 機能単位の思考: ユーザー登録機能だけを実装async function registerUser(email: string, password: string) { const user = await db.users.create({ email, password }); return user;}シニアの思考:
// システム単位の思考: 運用・障害・将来変更まで含めて設計class UserRegistrationService { constructor( private db: Database, private emailService: EmailService, private auditLog: AuditLogService, private rateLimiter: RateLimiter ) {}
async registerUser(email: string, password: string): Promise<User> { // 1. レート制限チェック await this.rateLimiter.check(email);
// 2. トランザクション境界の設計 return await this.db.transaction(async (tx) => { // 3. 冪等性の確保(重複登録の防止) const existingUser = await tx.users.findByEmail(email); if (existingUser) { throw new DuplicateUserError('User already exists'); }
// 4. データ整合性の確保 const user = await tx.users.create({ email, password });
// 5. 監査ログの記録 await this.auditLog.record('user_registered', { userId: user.id });
return user; }); }}運用・障害・将来変更まで含めた設計
Section titled “運用・障害・将来変更まで含めた設計”1. データの整合性を守る「最後の砦」:トランザクション設計
Section titled “1. データの整合性を守る「最後の砦」:トランザクション設計”トランザクション設計において、シニアは「ACID特性」をアプリケーションの文脈でどう実現するかを考えます。
境界の定義
どこまでを一蓮托生にするかが重要です。外部API(決済等)をトランザクションに含めないのは、外部の遅延がDBのコネクションを占有し、システム全体を道連れにするのを防ぐためです。
ロック戦略
「楽観ロック」はユーザーの体験を維持しつつスケールさせ、「悲観ロック」は整合性が何より優先される「金銭等のシビアな更新」に限定して使用します。この使い分けがシステムの寿命を決めます。
class OrderService { async createOrder(userId: number, items: OrderItem[]): Promise<Order> { // トランザクション境界: 注文作成と在庫減算を同一トランザクションで return await this.db.transaction(async (tx) => { // 1. 在庫チェックと減算(楽観ロック) for (const item of items) { const product = await tx.products.findById(item.productId); if (product.stock < item.quantity) { throw new InsufficientStockError(); } await tx.products.updateStock(item.productId, -item.quantity); }
// 2. 注文作成 const order = await tx.orders.create({ userId, items });
return order; }); }}2. 「再試行可能」な世界を作る:冪等性の威力
Section titled “2. 「再試行可能」な世界を作る:冪等性の威力”分散システムにおいて、ネットワークは常に不安定です。「リクエストが届いたか不明」な時、クライアントは再送します。
Idempotency Key(冪等性キー)
シニアの設計には必ず「二重実行」への耐性があります。これにより、決済の重複や過剰な在庫減算を「コードの構造」として不可能にします。
class PaymentService { async processPayment( orderId: number, amount: number, idempotencyKey: string ): Promise<Payment> { // 1. 冪等性キーで重複チェック const existingPayment = await this.db.payments.findByKey(idempotencyKey); if (existingPayment) { return existingPayment; // 既に処理済み }
// 2. トランザクション内で処理 return await this.db.transaction(async (tx) => { const payment = await tx.payments.create({ orderId, amount, idempotencyKey, status: 'processing', });
try { const result = await this.paymentGateway.charge(amount); await tx.payments.update(payment.id, { status: 'completed', transactionId: result.transactionId, }); return payment; } catch (error) { await tx.payments.update(payment.id, { status: 'failed' }); throw error; } }); }}3. ロック戦略:楽観・悲観ロックの使い分け
Section titled “3. ロック戦略:楽観・悲観ロックの使い分け”ロック戦略の選択は、システムの特性とビジネス要件に基づいて判断します。
| ロック種別 | 適用場面 | メリット | デメリット |
|---|---|---|---|
| 楽観ロック | 競合が少ない、読み取り中心 | 高スループット、スケーラブル | 競合時に再試行が必要 |
| 悲観ロック | 競合が多い、金銭処理等 | 整合性が確実 | ロック待ちでスループット低下 |
// 楽観ロック: 競合が少ない場合(読み取りが多い)class OptimisticLockingService { async updateUser(userId: number, data: UserData): Promise<User> { const user = await this.db.users.findById(userId);
// バージョン番号で楽観ロック const updated = await this.db.users.update( { id: userId, version: user.version }, { ...data, version: user.version + 1 } );
if (!updated) { throw new ConcurrentModificationError(); }
return updated; }}
// 悲観ロック: 競合が多い場合(書き込みが多い)class PessimisticLockingService { async updateBalance(accountId: number, amount: number): Promise<void> { // SELECT FOR UPDATEで悲観ロック return await this.db.transaction(async (tx) => { const account = await tx.accounts.findByIdForUpdate(accountId);
if (account.balance + amount < 0) { throw new InsufficientBalanceError(); }
await tx.accounts.update(accountId, { balance: account.balance + amount, }); }); }}パフォーマンス・スケーラビリティ
Section titled “パフォーマンス・スケーラビリティ”class ProductService { constructor( private db: Database, private cache: Cache, private searchIndex: SearchIndex ) {}
async getProduct(id: number): Promise<Product> { // 1. キャッシュから取得 const cached = await this.cache.get(`product:${id}`); if (cached) { return cached; }
// 2. データベースから取得(インデックスを使用) const product = await this.db.products.findById(id);
// 3. キャッシュに保存 await this.cache.set(`product:${id}`, product, 3600);
return product; }}「見えないもの」を可視化する:可観測性(Observability)
Section titled “「見えないもの」を可視化する:可観測性(Observability)”動いているからOK、ではなく「どう動いているか、なぜ壊れたか」が瞬時にわかることがシニアの品質です。
3つの柱(ログ・メトリクス・トレース)
単なるエラーログではなく、リクエストを跨いで追跡できる TraceID を埋め込み、ダッシュボードで異常なスパイク(メトリクス)を検知します。これが「火消し」を迅速化する最強の備えです。
| 柱 | 役割 | 具体例 |
|---|---|---|
| ログ | 何が起きたかの記録 | エラーメッセージ、処理内容 |
| メトリクス | 数値で傾向を把握 | レスポンスタイム、エラー率 |
| トレース | リクエストの追跡 | TraceID による処理フロー可視化 |
class ObservableService { constructor( private logger: Logger, private metrics: Metrics, private tracer: Tracer ) {}
async processOrder(orderId: number): Promise<void> { const span = this.tracer.startSpan('process_order'); span.setTag('order.id', orderId);
try { this.metrics.increment('orders.processing'); const startTime = Date.now();
this.logger.info('Processing order', { orderId });
await this.doProcessOrder(orderId);
this.metrics.increment('orders.completed'); this.metrics.histogram('orders.processing.duration', Date.now() - startTime);
span.setStatus({ code: SpanStatusCode.OK }); } catch (error) { this.logger.error('Order processing failed', { orderId, error }); this.metrics.increment('orders.failed'); span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); throw error; } finally { span.finish(); } }}設計判断の文書化
Section titled “設計判断の文書化”設計判断の例
Section titled “設計判断の例”## 設計判断: 楽観ロック vs 悲観ロック
### 選択: 楽観ロックを採用
### 理由:1. **競合頻度が低い**: ユーザー情報の更新は1ユーザーあたり1日1回程度2. **パフォーマンス**: 悲観ロックはロック待ちが発生し、スループットが低下3. **スケーラビリティ**: 楽観ロックは読み取りが多い場合に有利
### トレードオフ:- **メリット**: 高いスループット、スケーラビリティ- **デメリット**: 競合時は再試行が必要(UXへの影響は軽微)
### 代替案:- 悲観ロック: 競合が多い場合に有効だが、このケースでは不要- 最終書き込み勝利: シンプルだがデータ損失のリスク設計思考のパラダイム:正しいコードより「堅牢な構造」
Section titled “設計思考のパラダイム:正しいコードより「堅牢な構造」”ミドルとシニアの設計アプローチの違いを整理します。
| 観点 | ミドルの実装(機能中心) | シニアの設計(システム中心) |
|---|---|---|
| エラー処理 | try-catch でログを出すだけ | 整合性を戻す(補償トランザクション)まで考慮 |
| パフォーマンス | 計算量を最適化する | キャッシュ、DBインデックス、非同期処理を組み合わせる |
| スケーラビリティ | インスタンスを増やせばいいと考える | ステートレスな設計とDBのロック競合回避を優先する |
| ドキュメント | 「何をしているか」を書く | 「なぜこの道を選び、何を選ばなかったか(ADR)」を書く |
まとめ:設計力とは想像力の解像度
Section titled “まとめ:設計力とは想像力の解像度”「シニアのコードが複雑に見えるのは、彼らが『現実世界の複雑さ』をあらかじめコードに閉じ込めているからです。運用が始まった後にコードを直すのは、飛行中のエンジンの修理と同じ。離陸前に(設計段階で)、どれだけ『最悪の事態』をシミュレーションできるか。その想像力の解像度が、貴方の設計力そのものです。」
シニアエンジニアの設計は、単なるコードの品質ではなく、システム全体の信頼性・運用性・将来の変更容易性を「構造」として担保することにあります。技術的な判断の背後には、常に「なぜそうするのか」という思想があり、その思想を理解することがシニアエンジニアへの成長の鍵となります。