UnlockOS Developers
← 記事一覧に戻る
🛡️

堅牢な予約システム構築:状態管理と競合状態

2026年6月1日2026年6月7日
6
117 commits
深度 8/10
state-managementrace-conditionssecuritytypescripterror-handling

堅牢な予約システム構築:状態管理と競合状態

はじめに

スマートロック管理のようなセキュリティが重要なシステムにおいて、二重予約の防止と一貫した状態の維持は、単なるユーザーエクスペリエンスの問題ではなく、物理的なセキュリティの問題です。複数のユーザーが同時に同じリソースを予約しようとする際、アクセス制御を危険にさらす可能性のある競合を防ぐため、堅牢な状態管理が不可欠になります。

この記事では、予約の競合を処理するための高度なパターン、適切なRBACガードの実装、そして分散システムにおける復旧性の高い決済フローの構築について探究します。

集中型Intent管理による二重予約の防止

予約システムの最も重要な側面の一つは、二重予約につながる可能性のある競合状態の防止です。集中型のintent重複検出システムは堅牢なソリューションを提供します:

interface ReservationIntent {
  id: string;
  resourceId: string;
  timeSlot: TimeRange;
  userId: string;
  status: 'pending' | 'confirmed' | 'cancelled';
  createdAt: Date;
}

class ReservationManager {
  async preventOverlapConflict(
    intent: ReservationIntent,
    skipOverlapTrigger: boolean = false
  ): Promise<void> {
    if (skipOverlapTrigger) {
      return; // 特定のシナリオでのバイパスを許可
    }

    const overlappingIntents = await this.findOverlappingIntents(
      intent.resourceId,
      intent.timeSlot
    );

    if (overlappingIntents.length > 0) {
      throw new ConflictError({
        code: 'RESERVATION_OVERLAP',
        message: 'Resource already reserved for this time slot',
        conflictingIntents: overlappingIntents.map(i => i.id)
      });
    }
  }

  private async findOverlappingIntents(
    resourceId: string,
    timeSlot: TimeRange
  ): Promise<ReservationIntent[]> {
    return this.intentRepository.findWhere({
      resourceId,
      status: ['pending', 'confirmed'],
      timeSlot: {
        overlaps: timeSlot
      }
    });
  }
}

このアプローチには以下のセキュリティ上の利点があります:

  • アトミックな競合検出:データベースレベルでの競合状態の防止
  • 監査証跡:すべての予約試行がintentレコードとしてログ記録される
  • 段階的劣化:特定のシナリオでの制御されたバイパス

曜日制限プラン:時間ベースのアクセス制御

時間ベースのアクセス制限の実装は、セキュリティ制御の追加層を提供します。このパターンは、スケジュールに基づいて異なるアクセスポリシーが必要な施設にとって特に価値があります:

interface PlanAvailability {
  availableDaysOfWeek: number[]; // 0=日曜日, 1=月曜日, etc.
  availableOnHoliday: boolean;
}

class TemporalAccessValidator {
  validatePlanAvailability(
    plan: PlanAvailability,
    requestedDate: Date,
    isHoliday: boolean
  ): ValidationResult {
    // 祝日は曜日ルールよりも優先される
    if (isHoliday) {
      if (!plan.availableOnHoliday) {
        return {
          isValid: false,
          error: 'Plan not available on holidays',
          code: 'HOLIDAY_RESTRICTED'
        };
      }
      return { isValid: true };
    }

    const dayOfWeek = requestedDate.getDay();
    if (!plan.availableDaysOfWeek.includes(dayOfWeek)) {
      return {
        isValid: false,
        error: 'Plan not available on this day of week',
        code: 'DAY_RESTRICTED'
      };
    }

    return { isValid: true };
  }

  // バリデーションをバイパスする可能性のある多次元配列を防ぐ
  validateAvailableDays(days: unknown): number[] {
    if (!Array.isArray(days)) {
      throw new ValidationError('Available days must be an array');
    }

    // ネストした配列を拒否
    if (days.some(day => Array.isArray(day))) {
      throw new ValidationError('Multidimensional arrays not allowed');
    }

    return days.filter(day => 
      Number.isInteger(day) && day >= 0 && day <= 6
    );
  }
}

ロールベースアクセス制御(RBAC)ルートガード

適切なRBACの実装には、複数の層での強制が必要です。ルートレベルのガードは第一線の防御を提供します:

interface UserRole {
  name: string;
  permissions: string[];
  routes: string[];
}

class RBACRouteGuard {
  private rolePermissions: Map<string, UserRole> = new Map();

  enforceRouteAccess(userRole: string, requestedRoute: string): boolean {
    const role = this.rolePermissions.get(userRole);
    if (!role) {
      throw new AuthorizationError('Invalid role');
    }

    // 完全なルートマッチ
    if (role.routes.includes(requestedRoute)) {
      return true;
    }

    // 動的ルートのパターンベースマッチング
    const hasPatternMatch = role.routes.some(pattern => {
      const regex = new RegExp(pattern.replace('*', '.*'));
      return regex.test(requestedRoute);
    });

    if (!hasPatternMatch) {
      throw new AuthorizationError({
        code: 'ROUTE_ACCESS_DENIED',
        message: `Role '${userRole}' cannot access route '${requestedRoute}'`,
        userRole,
        requestedRoute
      });
    }

    return true;
  }

  // 階層的権限のための施設スコープオーバーライドパターン
  checkFacilityOverride(
    userRole: string,
    facilityId: string,
    action: string
  ): boolean {
    const overrideKey = `${facilityId}:${action}`;
    const role = this.rolePermissions.get(userRole);
    
    return role?.permissions.includes(overrideKey) ?? false;
  }
}

復旧性の高い決済リカバリパターン

予約システムにおける決済の失敗には、料金徴収は成功したが確認が失敗したエッジケースを処理するための洗練されたリカバリメカニズムが必要です:

interface PaymentIntent {
  id: string;
  reservationId: string;
  amount: number;
  status: 'requires_payment' | 'processing' | 'succeeded' | 'requires_action';
  stripeCustomerId?: string;
}

class PaymentRecoveryManager {
  async recoverChargedButUnconfirmed(): Promise<void> {
    // 決済は成功したが予約が確認されていない予約を検索
    const orphanedPayments = await this.findOrphanedSuccessfulPayments();

    for (const payment of orphanedPayments) {
      try {
        await this.finalizeReservation(payment.reservationId);
        await this.logRecoveryAction(payment.id, 'auto_recovered');
      } catch (error) {
        await this.handleRecoveryFailure(payment, error);
      }
    }
  }

  async resumePaymentFlow(
    reservationId: string,
    customerId?: string
  ): Promise<PaymentIntent> {
    const reservation = await this.getIncompleteReservation(reservationId);
    
    // 保存されたカード機能のためにPaymentIntentに顧客をアタッチ
    const paymentIntent = await this.stripeClient.paymentIntents.update(
      reservation.paymentIntentId,
      {
        customer: customerId,
        // LinkおよびCard保存機能を有効化
        payment_method_options: {
          card: {
            setup_future_usage: 'off_session'
          }
        }
      }
    );

    // 再開されたフローを反映するためにローカル状態を更新
    await this.updateReservationStatus(reservationId, 'payment_resumed');
    
    return paymentIntent;
  }

  private async handleRecoveryFailure(
    payment: PaymentIntent,
    error: Error
  ): Promise<void> {
    await this.auditLogger.error({
      event: 'payment_recovery_failed',
      paymentId: payment.id,
      reservationId: payment.reservationId,
      error: error.message,
      timestamp: new Date().toISOString()
    });

    // 手動レビューにエスカレーション
    await this.escalateToManualReview(payment, error);
  }
}

段階的ロールアウトのためのFeature Flag管理

セキュリティが重要な機能には、慎重なロールアウト戦略が必要です。Feature flagは、システムの安定性を維持しながら制御されたデプロイメントを可能にします:

class FeatureRolloutManager {
  private facilityFeatures: Map<string, Set<string>> = new Map();

  enableFeatureForFacilities(
    feature: string,
    facilities: string[],
    validationRules?: ValidationRule[]
  ): void {
    // 有効化前に機能ロールアウトを検証
    if (validationRules) {
      for (const rule of validationRules) {
        if (!rule.validate(facilities)) {
          throw new ValidationError(rule.message);
        }
      }
    }

    facilities.forEach(facilityId => {
      if (!this.facilityFeatures.has(facilityId)) {
        this.facilityFeatures.set(facilityId, new Set());
      }
      this.facilityFeatures.get(facilityId)!.add(feature);
    });

    this.auditFeatureChange('enable', feature, facilities);
  }

  isFeatureEnabled(facilityId: string, feature: string): boolean {
    const features = this.facilityFeatures.get(facilityId);
    return features?.has(feature) ?? false;
  }

  private auditFeatureChange(
    action: 'enable' | 'disable',
    feature: string,
    facilities: string[]
  ): void {
    this.auditLogger.info({
      event: 'feature_flag_change',
      action,
      feature,
      facilities,
      timestamp: new Date().toISOString()
    });
  }
}

まとめ

復旧性の高い予約システムの構築には、セキュリティと信頼性の複数の層への注意が必要です:

  1. 競合防止:集中型intent管理により、リソース割り当てを危険にさらす可能性のある競合状態を防止
  2. 時間的制御:曜日と祝日制限により、きめ細かいアクセス制御を提供
  3. RBACガード:多層認可により、機密操作への不正アクセスを防止
  4. 決済リカバリ:洗練されたエラー処理により、決済フローでの取引損失を防止
  5. 段階的ロールアウト:Feature flagにより、セキュリティが重要な機能の安全なデプロイメントを実現

これらのパターンは、堅牢な状態管理と包括的なエラー処理が、セキュリティの整合性を維持しながらエッジケースを優雅に処理できる信頼性の高いシステムを作成する方法を示しています。

主要な発見

1
セキュリティ

集中型Intent重複検出

包括的な監査証跡とともに、データベースレベルでのアトミックな競合検出により二重予約の競合状態を防止します

2
アクセス制御

時間ベースアクセス制限

曜日と祝日制御による時間ベースのセキュリティポリシーを実装し、許可された期間外での不正アクセスを防止します

3
認可

多層RBACガード

動的なアクセス制御のためのパターンベースマッチングを備えた、ルートレベルおよび施設スコープの権限強制を実現します

4
信頼性

決済リカバリメカニズム

決済のエッジケースに対する洗練されたエラー処理、課金済みだが未確認の取引の自動リカバリを含みます