ストーリー
高橋アーキテクト が最終課題を示しました。
ミッション概要
| 項目 | 内容 |
|---|---|
| 題材 | 配車サービス(タクシー配車アプリ) |
| 目標 | Month 5で学んだ全スキルを統合した分散システム設計 |
| 所要時間 | 90分 |
| 成果物 | 完全なアーキテクチャ設計書 |
システム要件
機能要件
- 乗客機能: 配車リクエスト、乗車地/目的地指定、運賃見積もり、乗車中の追跡
- ドライバー機能: 配車リクエストの受諾/拒否、位置情報の送信、乗車完了報告
- マッチング: 最寄りのドライバーを自動マッチング
- 決済: 乗車完了後の自動決済
- 評価: 乗客とドライバーの相互評価
- 通知: 配車確定、ドライバー到着、乗車完了の通知
非機能要件
| 項目 | 要件 |
|---|---|
| レイテンシ | 配車リクエスト → マッチング: 5秒以内 |
| スループット | ピーク時 10,000リクエスト/分 |
| 可用性 | 99.9% |
| データ整合性 | 決済は二重課金を絶対に防ぐ |
| 位置情報 | ドライバー位置はリアルタイム更新(1秒ごと) |
ミッション 1: マイクロサービス分割(15分)
配車サービスを適切なマイクロサービスに分割してください。
設計すべきこと:
- サービス一覧と各サービスの責務
- 各サービスが所有するデータ
- 技術スタック(言語、DB)の選定理由
ヒントと模範回答
サービス分割:
1. Passenger Service
責務: 乗客のプロフィール管理、認証
データ: passengers, payment_methods
技術: TypeScript + PostgreSQL
2. Driver Service
責務: ドライバーのプロフィール、車両情報
データ: drivers, vehicles, documents
技術: TypeScript + PostgreSQL
3. Ride Service
責務: 乗車のライフサイクル管理
データ: rides, ride_status_history
技術: TypeScript + PostgreSQL(Saga Orchestrator)
4. Matching Service
責務: ドライバーと乗客のマッチング
データ: matching_requests, match_results
技術: Go + Redis(高速位置検索)
5. Location Service
責務: リアルタイム位置情報管理
データ: driver_locations (揮発性)
技術: Go + Redis GEO(地理空間インデックス)
6. Pricing Service
責務: 運賃計算(距離、時間、需要に基づく動的価格)
データ: pricing_rules, surge_zones
技術: TypeScript + DynamoDB
7. Payment Service
責務: 決済処理、返金
データ: payments, refunds, receipts
技術: Java + PostgreSQL(高い信頼性)
8. Rating Service
責務: 評価管理
データ: ratings, reviews
技術: TypeScript + DynamoDB
9. Notification Service
責務: プッシュ通知、SMS
データ: notification_logs
技術: TypeScript + SQS
ミッション 2: 通信設計とイベントフロー(15分)
各サービス間の通信方式(同期/非同期)とイベントフローを設計してください。
設計すべきこと:
- 同期通信が必要な箇所と理由
- 非同期通信が適切な箇所と理由
- 主要なイベントの一覧とスキーマ
ヒントと模範回答
同期通信:
乗客 → Pricing Service: 運賃見積もり(即座に結果が必要)
Ride Service → Matching Service: マッチングリクエスト(5秒以内)
Ride Service → Location Service: ドライバー位置の取得
非同期通信(イベント):
ride.requested → Matching Serviceがマッチング開始
ride.matched → Notification Serviceが乗客・ドライバーに通知
ride.started → Location Serviceが追跡開始
ride.completed → Payment Serviceが決済開始
→ Rating Serviceが評価リクエスト
payment.completed → Notification Serviceが領収書送信
payment.failed → Ride Serviceが決済リトライ
メッセージブローカー: Apache Kafka
Topics:
- ride-events (key: rideId, partitions: 24)
- payment-events (key: rideId, partitions: 12)
- location-events (key: driverId, partitions: 48)
- notification-events (partitions: 12)
ミッション 3: Saga設計(配車→乗車→決済フロー)(20分)
配車リクエストから決済完了までのSagaを設計してください。
設計すべきこと:
- Sagaのタイプ選択(Choreography / Orchestration)と理由
- 各ステップの正常系と補償処理
- タイムアウトと障害時の対応
ヒントと模範回答
// Orchestration を選択
// 理由: ステップが多く、タイムアウト管理が重要。
// 乗車のライフサイクル全体を1箇所で管理したい。
const rideSagaSteps: SagaStep[] = [
{
name: "createRide",
execute: async (ctx) => {
const ride = await rideService.create({
passengerId: ctx.passengerId,
pickup: ctx.pickup,
destination: ctx.destination,
});
return { rideId: ride.id };
},
compensate: async (ctx) => {
await rideService.cancel(ctx.rideId, "SYSTEM_CANCELLED");
},
timeout: 5000,
},
{
name: "estimatePrice",
execute: async (ctx) => {
const estimate = await pricingService.estimate({
pickup: ctx.pickup,
destination: ctx.destination,
});
return { estimatedFare: estimate.fare };
},
compensate: async () => { /* 読み取りのみ、補償不要 */ },
timeout: 3000,
},
{
name: "matchDriver",
execute: async (ctx) => {
const match = await matchingService.findDriver({
rideId: ctx.rideId,
pickup: ctx.pickup,
maxWaitSeconds: 30,
});
return { driverId: match.driverId };
},
compensate: async (ctx) => {
await matchingService.releaseDriver(ctx.driverId);
},
timeout: 30000, // マッチングは最大30秒待つ
},
{
name: "confirmRide",
execute: async (ctx) => {
await rideService.confirm(ctx.rideId, ctx.driverId);
// → ride.matched イベントが発行される
return {};
},
compensate: async (ctx) => {
await rideService.cancel(ctx.rideId, "MATCH_CANCELLED");
},
timeout: 5000,
},
// 乗車完了後(ドライバーがride.completedをトリガー)
{
name: "processPayment",
execute: async (ctx) => {
const payment = await paymentService.charge({
rideId: ctx.rideId,
passengerId: ctx.passengerId,
amount: ctx.finalFare,
idempotencyKey: `ride-${ctx.rideId}-payment`,
});
return { paymentId: payment.id };
},
compensate: async (ctx) => {
if (ctx.paymentId) {
await paymentService.refund(ctx.paymentId);
}
},
timeout: 10000,
},
{
name: "notifyCompletion",
execute: async (ctx) => {
await notificationService.send(ctx.passengerId, {
type: "RIDE_COMPLETED",
rideId: ctx.rideId,
fare: ctx.finalFare,
});
return {};
},
compensate: async () => { /* 通知は不可逆、最後に配置 */ },
timeout: 5000,
},
];
ミッション 4: CQRS設計(位置情報とライド履歴)(15分)
位置情報のリアルタイム表示と乗車履歴の表示をCQRSで設計してください。
設計すべきこと:
- Write Model(位置情報の書き込み、乗車記録)
- Read Model(地図上のドライバー表示、乗車履歴画面)
- プロジェクションの設計
ヒントと模範回答
// Write Model: ドライバー位置(1秒ごとに更新)
// → Redis GEO(揮発性OK、Write性能最優先)
class LocationWriteService {
async updateLocation(driverId: string, lat: number, lng: number): Promise<void> {
await this.redis.geoadd("driver_locations", lng, lat, driverId);
// Outbox不要: 位置情報はロスト許容(次の更新で上書き)
}
}
// Read Model 1: 周辺ドライバーの地図表示
// → Redis GEO から直接クエリ(Write = Read、分離不要)
class NearbyDriversQuery {
async find(lat: number, lng: number, radiusKm: number): Promise<Driver[]> {
return await this.redis.georadius("driver_locations", lng, lat, radiusKm, "km");
}
}
// Write Model: 乗車記録
// → PostgreSQL(ACID、正規化)
// Read Model: 乗車履歴画面
// → DynamoDB(userId で高速クエリ)
interface RideHistoryReadModel {
passengerId: string;
rideId: string;
driverName: string;
driverPhoto: string;
vehicleInfo: string;
pickup: string;
destination: string;
fare: number;
rating: number;
date: string;
}
// プロジェクション: ride.completed → 乗車履歴Read Model更新
class RideHistoryProjection {
async handleRideCompleted(event: RideCompletedEvent): Promise<void> {
const driver = await this.driverService.getDriver(event.data.driverId);
await this.dynamodb.put({
TableName: "ride_history",
Item: {
passengerId: event.data.passengerId,
rideId: event.data.rideId,
driverName: driver.name,
driverPhoto: driver.photoUrl,
vehicleInfo: `${driver.vehicle.make} ${driver.vehicle.model}`,
pickup: event.data.pickupAddress,
destination: event.data.destinationAddress,
fare: event.data.fare,
date: event.metadata.timestamp,
},
});
}
}
ミッション 5: テスト・運用戦略(10分)
このシステムのテスト戦略と運用戦略を設計してください。
ヒントと模範回答
テスト戦略:
1. Contract Tests
- Ride Service ↔ Payment Service の決済API契約
- Ride Service ↔ Matching Service のマッチングAPI契約
- イベントスキーマ(ride.completed等)の契約
2. Chaos Engineering
実験1: Matching Service停止
仮説: 配車リクエストがタイムアウトし「ドライバーが見つかりません」表示
検証: 乗客UIにエラーメッセージ、データ不整合なし
実験2: Payment Service 高遅延(10秒)
仮説: 乗車完了は即座に表示、決済は非同期で再処理
検証: 決済が最終的に完了、二重課金なし
実験3: Kafka障害
仮説: Outboxパターンでイベントが保持され、復旧後に処理
検証: イベントのロストなし
3. オブザーバビリティ
- 分散トレーシング: rideIdをcorrelation IDとして全サービスで追跡
- メトリクス: マッチング所要時間、決済成功率、ドライバー稼働率
- ログ: 構造化ログ + ELKで集約
ミッション 6: アーキテクチャ全体図(15分)
すべてを統合したアーキテクチャ図をテキストで描いてください。
ヒントと模範回答
[Mobile App (Passenger)] [Mobile App (Driver)]
│ │
[API Gateway] │
├─ 認証 (JWT) │
├─ レート制限 │
└─ WebSocket (位置情報) │
│ │
┌──────────────────┼────────────────────────┘
│ │
[Passenger] [Ride Service] [Driver]
[Service] (Saga Orchestrator) [Service]
│ │ │ │ │
│ 同期 │ │ │ 同期 │
│ ┌──────────┘ │ └──────────┐ │
│ │ │ │ │
[Pricing] [Matching] [Location]
[Service] [Service] [Service]
│ [Redis GEO]
│
[Kafka]
┌────────┼────────┐
│ │ │
[Payment] [Rating] [Notification]
[Service] [Service] [Service]
Write DB: PostgreSQL (rides, payments)
Read DB: DynamoDB (乗車履歴), Redis (位置, マッチング)
イベント: Kafka (ride-events, payment-events, location-events)
監視: OpenTelemetry + Jaeger + Prometheus + Grafana
達成チェックリスト
- 8つ以上のマイクロサービスに適切に分割できた
- 同期/非同期通信とイベントフローを設計できた
- 配車→決済のSagaをOrchestrationで設計できた
- 位置情報と乗車履歴のCQRSを設計できた
- テスト・運用戦略(Contract Testing, Chaos Engineering)を策定できた
- 全体のアーキテクチャ図を描けた
推定所要時間: 90分