はじめに
この記事は、エアークローゼットアドベントカレンダー2025の日18目の記事です。
レンタルやホテル予約、レンタカーなどのレンタルビジネスでは、「いつ、何が、いくつ利用可能か」を正確に把握することが非常に重要です。
従来の小売業のような「在庫数を単純にカウントする」方式では、レンタルビジネスの特性に対応できません。なぜなら、同じ商品が何度も貸し出され、返却されるからです。
本記事では、レンタルビジネスにおける在庫管理の手法として ATP(Available to Promise) に基づく2つのアプローチを比較し、より効率的なInterval-based Inventory(区間ベース在庫管理)への移行について解説します。
現行システムの概要
Daily Stock Records方式
現行のシステムでは、日付ごとに在庫レコードを保持する方式を採用しています。
coordinate_style_id: "DRESS-A"
date: "2024-12-20" → stock: 5
date: "2024-12-21" → stock: 5
date: "2024-12-22" → stock: 4 ← 1着予約済み
date: "2024-12-23" → stock: 4
date: "2024-12-24" → stock: 4
date: "2024-12-25" → stock: 5 ← 返却済み
この方式では、各商品の各日付に対して1つのレコードが存在し、その日の利用可能数を直接示しています。
アーキテクチャの特徴
現行システムは以下の技術スタックで構成されています:
| コンポーネント | 技術 | 用途 |
|---|---|---|
| マスターデータ | MySQL | 商品情報、SKU、注文データ |
| 在庫・イベント | MongoDB | 日次在庫、イベントソーシング |
| 外部連携 | Warehouse API | 倉庫システムとの在庫同期 |
Event Sourcingパターンを採用しており、在庫の変更履歴を完全に追跡できる点が特徴です。
Daily Stock Recordsの強みと弱み
強み
| 項目 | 説明 |
|---|---|
| 直感的なクエリ | 特定日の在庫は1回のクエリで即座に取得可能 |
| 日別価格設定 | 在庫と価格を同じレコードで管理でき、繁忙期・閑散期の価格設定が容易 |
| 監査証跡 | Event Sourcingにより全ての変更履歴を追跡可能 |
| 事前計算済み | 在庫数が事前に計算されているため、読み取り時の計算コストが低い |
弱み
| 項目 | 説明 |
|---|---|
| ストレージ肥大化 | 1商品 × 365日 = 365レコード/年。1000商品なら36.5万レコード/年 |
| 更新の複雑さ | 1件の予約で複数日分のレコードを更新する必要がある |
| キャンセル処理 | 予約キャンセル時に該当する全日付のレコードを復元する必要がある |
| 日付変更の困難さ | 予約日程の変更には、旧日程の復元と新日程の減算が必要 |
| 同期の複雑さ | 外部システムとの同期時に大量のレコード更新が発生 |
| 整合性リスク | 複数レコードの更新中に障害が発生すると不整合が生じる可能性 |
具体的な問題シナリオ
予約変更の例:
ユーザーが12/22〜12/25の予約を12/26〜12/29に変更したい場合:
1. 12/22, 12/23, 12/24, 12/25 の在庫を +1 (4レコード更新)
2. 12/26, 12/27, 12/28, 12/29 の在庫を -1 (4レコード更新)
合計8レコードの更新が必要
この処理中に障害が発生すると、在庫データの不整合が生じます。
提案するアプローチ:Interval-based Inventory
基本概念
Interval-based Inventory(区間ベース在庫管理)は、日次の在庫数ではなく、予約(Booking)そのものを記録し、区間ごとの在庫数を事前計算して保持する方式です。
商品マスター:
coordinate_style_id: "DRESS-A"
total_stock: 5
予約データ:
booking_id: "B001"
coordinate_style_id: "DRESS-A"
start_date: "2024-12-22"
end_date: "2024-12-25"
status: "confirmed"
事前計算された区間データ(Batch処理で生成):
[12/20, 12/21]: available_stock: 5
[12/22, 12/25]: available_stock: 4
[12/26, 12/31]: available_stock: 5
重要なポイント:在庫確認時に計算は行わない。事前計算された区間データをクエリするだけ。
なぜこの方式が優れているのか
| 観点 | Daily Records | Interval-based |
|---|---|---|
| ストレージ | O(商品数 × 日数) | O(予約の境界数) |
| 新規予約 | N日分のレコード更新 | 1レコード挿入 + Batch再計算 |
| キャンセル | N日分のレコード更新 | 1レコードのステータス変更 + Batch再計算 |
| 在庫確認 | 日付でクエリ | 区間でクエリ(事前計算済み) |
| 整合性 | 複数レコードの原子性が課題 | 予約は単一レコード、区間はBatchで再構築 |
コアロジック:Overlap・Split・Merge
Interval-based Inventoryの核心は、時間区間(Interval)の操作にあります。ここでは3つの重要なロジックを詳しく解説します。
全体像:Batch処理と在庫確認の分離
1. Overlap判定(重複検出)
2つの期間が重複しているかを判定するロジックです。
重複の条件:
期間Aと期間Bが重複する ⟺ A.start ≤ B.end AND A.end ≥ B.start
用途:
- 在庫確認時に、リクエスト期間と重なる区間を検索する
- Batch処理時に、予約同士の重なりを検出する
リクエスト期間: |-------| (12/22 - 12/25)
区間1: |-------| (12/20 - 12/23) → 重複あり
区間2: |-------| (12/24 - 12/27) → 重複あり
区間3: |---| (12/28 - 12/30) → 重複なし
タイムライン: 20 21 22 23 24 25 26 27 28 29 30
2. Split(区間の分割)— 事前計算の核心
Splitの目的
Splitは複数の予約が存在する時間軸を分割し、「同じ在庫数を持つ期間」ごとにまとめる処理です。
重要:Splitは在庫確認時ではなく、Batch処理で事前に実行される。
なぜSplitが必要か?
複数の予約が部分的に重なる場合、重なり具合によって在庫の消費量が変わります。
総在庫: 5着
予約A: [12/20, 12/25]
予約B: [12/23, 12/28]
タイムライン:
20 21 22 23 24 25 26 27 28
| | | | | | | | |
予約A: ████████████████████████████
予約B: ████████████████████████
日付ごとの予約数:
12/20: 1件(Aのみ)
12/21: 1件(Aのみ)
12/22: 1件(Aのみ)
12/23: 2件(A + B) ← ここから在庫消費が増える
12/24: 2件(A + B)
12/25: 2件(A + B)
12/26: 1件(Bのみ) ← ここから在庫消費が減る
12/27: 1件(Bのみ)
12/28: 1件(Bのみ)
このままでは9日分のデータが必要ですが、Splitを使うと:
Split後の区間(Batch処理で生成):
区間1: [12/20, 12/22] → 予約1件 → 在庫: 4
区間2: [12/23, 12/25] → 予約2件 → 在庫: 3
区間3: [12/26, 12/28] → 予約1件 → 在庫: 4
9レコード → 3レコードに削減
複数予約が重なる場合
総在庫: 5着
予約A: [12/20, 12/26]
予約B: [12/22, 12/28]
予約C: [12/24, 12/30]
タイムライン:
20 21 22 23 24 25 26 27 28 29 30
| | | | | | | | | | |
予約A: ██████████████████████████████████
予約B: ██████████████████████████████
予約C: ██████████████████████████████
Batch処理によるSplit結果:
区間1: [12/20, 12/21] → 1件 → 在庫: 4
区間2: [12/22, 12/23] → 2件 → 在庫: 3
区間3: [12/24, 12/26] → 3件 → 在庫: 2 ← 最も混雑
区間4: [12/27, 12/28] → 2件 → 在庫: 3
区間5: [12/29, 12/30] → 1件 → 在庫: 4
在庫確認時の動作
在庫確認では計算しない。事前計算された区間をクエリするだけ。
ユーザーリクエスト: [12/23, 12/27] で予約したい
処理:
1. 事前計算された区間テーブルから、[12/23, 12/27]と重複する区間を取得
2. 該当区間:
区間2: [12/22, 12/23] → 在庫: 3
区間3: [12/24, 12/26] → 在庫: 2 ← 最小
区間4: [12/27, 12/28] → 在庫: 3
3. min(3, 2, 3) = 2 を返す
結果: 2着まで予約可能
なぜ「最小値」が重要か?
リクエスト期間: [12/23, 12/27]
各区間の利用可能数:
[12/23, 12/23]: 3着
[12/24, 12/26]: 2着 ← ボトルネック
[12/27, 12/27]: 3着
予約可能数 = min(3, 2, 3) = 2着
理由: 全期間を通して確実に確保できる数は、
最も在庫が少ない区間(ボトルネック)に制約される
3. Merge(区間の統合)
Mergeの目的
Split後に生成された区間のうち、隣接していて同じ在庫数を持つ区間を統合します。
これにより、クエリ対象のレコード数を削減できます。
重要:MergeもBatch処理で実行される。
なぜMergeが必要か?
Splitだけでは、予約の境界点ごとに区間が生成されます。しかし、在庫数が同じなら1つの区間として扱えるため、統合することでデータ量を削減できます。
具体例:予約が追加・キャンセルされた場合
シナリオ: 12月の予約状況
予約A: [12/05, 12/10] → キャンセル済み
予約B: [12/15, 12/20] → 確定
総在庫: 5着
Splitのみの場合(Mergeなし):
予約Aはキャンセル済みなので在庫に影響しないが、境界点は残る:
区間1: [12/01, 12/04]: 在庫5
区間2: [12/05, 12/10]: 在庫5 ← 予約Aはキャンセル済みなので在庫5のまま
区間3: [12/11, 12/14]: 在庫5
区間4: [12/15, 12/20]: 在庫4 ← 予約Bで在庫-1
区間5: [12/21, 12/31]: 在庫5
→ 5つの区間が生成される
Merge後:
同じ在庫数の隣接区間を統合:
区間1: [12/01, 12/14]: 在庫5 ← 区間1,2,3を統合
区間2: [12/15, 12/20]: 在庫4 ← 在庫数が異なるので統合しない
区間3: [12/21, 12/31]: 在庫5 ← 在庫数が異なるので統合しない
→ 3つの区間に削減
Merge処理の視覚化
Splitのみ(5区間):
|--在庫5--|--在庫5--|--在庫5--|--在庫4--|--在庫5--|
12/01 12/05 12/11 12/15 12/21
↓ Merge処理
Merge後(3区間):
|--------在庫5--------|--在庫4--|--在庫5--|
12/01 12/15 12/21
統合条件: 隣接 AND 同じ在庫数
別の例:複数予約の重なり
予約A: [12/10, 12/15]
予約B: [12/20, 12/25]
総在庫: 5着
Splitのみ:
[12/01, 12/09]: 在庫5
[12/10, 12/15]: 在庫4 ← 予約Aで-1
[12/16, 12/19]: 在庫5
[12/20, 12/25]: 在庫4 ← 予約Bで-1
[12/26, 12/31]: 在庫5
→ 5区間
Merge後:
[12/01, 12/09]: 在庫5
[12/10, 12/15]: 在庫4 ← 統合しない(次と在庫数が異なる)
[12/16, 12/19]: 在庫5 ← 統合しない(前後と在庫数が異なる)
[12/20, 12/25]: 在庫4 ← 統合しない(前と在庫数が異なる)
[12/26, 12/31]: 在庫5
→ この場合は5区間のまま(在庫数が交互に変わるため統合できない)
ポイント:Mergeは「隣接」かつ「同じ在庫数」の場合のみ統合する。
Mergeの効果
| 観点 | 効果 |
|---|---|
| ストレージ | キャンセルされた予約の境界点など、不要な区間を削減 |
| クエリ性能 | スキャン対象のレコード数が減少し、検索が高速化 |
| データ整合性 | 定期的なBatch処理で区間を再構築し、古いデータをクリーンアップ |
システムアーキテクチャ
Batch処理の役割
データフロー
区間テーブルの構造
Collection: inventory_intervals
{
coordinate_style_id: "DRESS-A",
intervals: [
{ start: "2024-12-01", end: "2024-12-10", available_stock: 5 },
{ start: "2024-12-11", end: "2024-12-20", available_stock: 3 },
{ start: "2024-12-21", end: "2024-12-31", available_stock: 5 }
],
total_stock: 5,
last_calculated_at: "2024-12-17T10:00:00Z"
}
Daily Records vs Interval-based 比較
まとめ
各方式の適用場面
| 方式 | 適している場面 |
|---|---|
| Daily Stock Records | 外部システムが日次在庫を提供する場合、日別の複雑な価格設定が必要な場合 |
| Interval-based | 予約期間が日〜週単位、キャンセル・変更が頻繁、ストレージ効率が重要 |
コアロジックの役割
| ロジック | 実行タイミング | 役割 |
|---|---|---|
| Overlap | 在庫確認時 | リクエスト期間と重なる区間を検索 |
| Split | Batch処理時 | 予約の境界点で時間軸を分割し、区間ごとの在庫を計算 |
| Merge | Batch処理時 | 同じ在庫数の隣接区間を統合し、レコード数を削減 |
重要な設計原則
在庫確認時に計算しない。
Batch処理で事前計算された区間データをクエリするだけ。
これにより:
- 在庫確認のレスポンスが高速(単純なクエリのみ)
- 複雑な計算はBatch処理に集約
- 予約の作成/変更/キャンセルは1レコードの操作で完結
レンタルビジネスにおける推奨
レンタルのようなビジネスでは、以下の理由からInterval-based Inventoryが適しています:
- レンタル期間が3〜7日程度 - 区間ベースの管理に最適
- 予約変更・キャンセルが発生しやすい - 単一レコード更新で完結
- メンテナンス期間の管理 - actual_end_dateで柔軟に対応
- データ整合性の担保 - 予約は原子的操作、区間はBatchで再構築
- スケーラビリティ - 読み取り負荷と計算負荷を分離
レンタルビジネスの在庫管理は、単なる数量管理ではなく「時間軸を考慮した可用性管理」です。
Split/Mergeによる事前計算とBatch処理による読み書き分離を組み合わせることで、複雑なレンタル予約の重なりを効率的に処理できるシステムを構築できます。
この記事が、皆さんのシステム設計の参考になれば幸いです。
最後までご覧いただきありがとうございました!
エアークローゼット Advent Calendar 2025はまだまだ続きますので、ぜひ他のエンジニア, デザイナー, PMの記事もご覧いただければと思います