Skip to content

技術力とシステム設計

ジュニアが「点(コード)」で書くのに対し、シニアは「線(時系列・状態)」と「面(システム全体・非機能要件)」で設計します。特に「冪等性」や「可観測性」をデフォルトの設計に組み込む姿勢は、運用フェーズでの「地獄」を未然に防ぐシニアの必須スキルです。

シニアエンジニアの設計は、ハッピーパス(正常系)よりも「アンハッピーパス(異常系)」に多くのリソースを割きます。これは、本番環境で発生する問題の大半が想定外の状況から生まれるためです。


機能単位ではなくシステム単位で考える

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();
}
}
}
## 設計判断: 楽観ロック vs 悲観ロック
### 選択: 楽観ロックを採用
### 理由:
1. **競合頻度が低い**: ユーザー情報の更新は1ユーザーあたり1日1回程度
2. **パフォーマンス**: 悲観ロックはロック待ちが発生し、スループットが低下
3. **スケーラビリティ**: 楽観ロックは読み取りが多い場合に有利
### トレードオフ:
- **メリット**: 高いスループット、スケーラビリティ
- **デメリット**: 競合時は再試行が必要(UXへの影響は軽微)
### 代替案:
- 悲観ロック: 競合が多い場合に有効だが、このケースでは不要
- 最終書き込み勝利: シンプルだがデータ損失のリスク

設計思考のパラダイム:正しいコードより「堅牢な構造」

Section titled “設計思考のパラダイム:正しいコードより「堅牢な構造」”

ミドルとシニアの設計アプローチの違いを整理します。

観点ミドルの実装(機能中心)シニアの設計(システム中心)
エラー処理try-catch でログを出すだけ整合性を戻す(補償トランザクション)まで考慮
パフォーマンス計算量を最適化するキャッシュ、DBインデックス、非同期処理を組み合わせる
スケーラビリティインスタンスを増やせばいいと考えるステートレスな設計とDBのロック競合回避を優先する
ドキュメント「何をしているか」を書く「なぜこの道を選び、何を選ばなかったか(ADR)」を書く

まとめ:設計力とは想像力の解像度

Section titled “まとめ:設計力とは想像力の解像度”

「シニアのコードが複雑に見えるのは、彼らが『現実世界の複雑さ』をあらかじめコードに閉じ込めているからです。運用が始まった後にコードを直すのは、飛行中のエンジンの修理と同じ。離陸前に(設計段階で)、どれだけ『最悪の事態』をシミュレーションできるか。その想像力の解像度が、貴方の設計力そのものです。」

シニアエンジニアの設計は、単なるコードの品質ではなく、システム全体の信頼性・運用性・将来の変更容易性を「構造」として担保することにあります。技術的な判断の背後には、常に「なぜそうするのか」という思想があり、その思想を理解することがシニアエンジニアへの成長の鍵となります。