目次
はじめに
Amazon SP-API で getOrders / getOrderItems を定期ポーリングして社内 OMS に取り込む処理は、一見シンプルに見える。しかし本番運用に入ると、以下の問題が次々と現れる。
- Lambda が途中でタイムアウトし、一部注文だけ登録された
- 同一の
AmazonOrderIdが 2 回 OMS に入り、2 重出荷になった - Amazon 側の遅延で
LastUpdatedAfterウィンドウをまたいだ注文が欠損した
これらをまとめて解決するのが 冪等設計 だ。
冪等性の原則
同じ入力を何度処理しても、最終状態が 1 回実行した結果と変わらない — これが冪等性の定義。注文同期で言えば「同じ AmazonOrderId を 100 回処理しても、OMS の注文レコードは 1 件だけ存在する」状態を保証する。
設計の全体像
SP-API getOrders
↓
SQS に AmazonOrderId を Enqueue(重複排除は SQS MessageDeduplicationId で)
↓
Lambda Worker: 1 メッセージ = 1 注文を処理
↓
DynamoDB: 条件付き Put("item NOT EXISTS" で重複防止)
↓
OMS へ書き込み(外部 API は Client-side idempotency key 付与)
実装パターン 1: SQS MessageDeduplicationId
FIFO キューの MessageDeduplicationId に AmazonOrderId を使うだけで、5 分以内の重複メッセージを SQS 側で排除できる。
await sqs.sendMessage({
QueueUrl: QUEUE_URL,
MessageGroupId: "orders",
MessageDeduplicationId: order.AmazonOrderId, // FIFO キューの重複排除キー
MessageBody: JSON.stringify({ orderId: order.AmazonOrderId }),
})実装パターン 2: DynamoDB 条件付き Put
5 分の SQS ウィンドウを超えた重複(リドライブやリプレイ)は DynamoDB 側で防ぐ。attribute_not_exists(pk) を条件に指定し、すでに存在するレコードへの上書きを拒否する。
await dynamodb.put({
TableName: "orders",
Item: { pk: orderId, ...orderData },
ConditionExpression: "attribute_not_exists(pk)",
}).catch((err) => {
if (err.name === "ConditionalCheckFailedException") {
// 既処理 → 正常終了扱い
return
}
throw err
})ConditionalCheckFailedException は「すでに処理済み」を意味するので、エラーとして再スローしてはいけない。
実装パターン 3: LastUpdatedAfter ウィンドウの重複許容
getOrders は LastUpdatedAfter でフィルタするが、境界付近の注文は前回と今回の両ウィンドウに含まれる。これは SP-API の仕様なので、ウィンドウを 10 分重複させて取得し、OMS 側の冪等 upsert に任せる。
const lastUpdatedAfter = new Date(
lastRunTime.getTime() - 10 * 60 * 1000 // 10 分のオーバーラップ
).toISOString()実装パターン 4: 外部 API への冪等キー付与
OMS が REST API の場合、リクエストごとに Idempotency-Key ヘッダを付与する。Lambda がタイムアウト後にリトライしても、同一キーなら OMS は同じレスポンスを返す。
const idempotencyKey = `sp-order-${orderId}`
await omsClient.post("/orders", payload, {
headers: { "Idempotency-Key": idempotencyKey },
})冪等キーは リトライをまたいで同じ値 になるよう、orderId など決定論的な値だけで構成する。Date.now() や UUID をキーに含めると、Lambda タイムアウト後のリトライ時に別のキーになり、OMS が別注文として扱ってしまう。
欠損対策: Checkpoint + Full Scan の組み合わせ
差分ポーリングだけに頼ると、Amazon 側の遅延やシステム障害で注文が欠損することがある。週次で CreatedAfter=7日前 のフルスキャンを走らせ、OMS に未登録の AmazonOrderId をバックフィルするジョブを入れておくと保険になる。
// 週次バックフィル: 差分ポーリングでこぼれた注文を拾う
async function weeklyBackfill() {
const createdAfter = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
for await (const order of paginate(ordersApi, { createdAfter })) {
const exists = await checkOmsExists(order.AmazonOrderId)
if (!exists) await enqueue(order.AmazonOrderId)
}
}まとめ
| 問題 | 対策 |
|---|---|
| Lambda タイムアウト後の重複 | SQS FIFO + DynamoDB 条件付き Put |
| ウィンドウ境界の欠損 | 10 分オーバーラップ |
| OMS への二重書き込み | Idempotency-Key ヘッダ |
| 差分ポーリングの欠損 | 週次フルスキャン バックフィル |




