Article

🔁 Lambdaの二重実行を防ぐ:Javaで冪等性を実装するPowertools入門

aws, lambda, java, dynamodb, powertools

AWS Lambdaを使っていると、「同じイベントがもう一度届いたらどうするか」を考える必要があります。

たとえば注文処理でLambdaが決済APIを呼び出した直後、レスポンスを返す前にタイムアウトしたとします。呼び出し元から見ると処理結果が分からないため再試行される可能性があります。そのとき何も対策していなければ、同じ注文を2回処理してしまうかもしれません。

このような問題を避けるために重要なのが**冪等性(idempotency)**です。冪等性とは、同じ操作を複数回実行しても、追加の副作用が発生しない性質です。

この記事では、JavaのLambdaでAWS Lambda PowertoolsのIdempotency機能を使い、重複実行を安全に扱う方法を整理します。

Lambdaでは「一度だけ実行される」と考えない

サーバーレスやメッセージングを使ったシステムでは、失敗時の再試行を前提に設計することが重要です。

たとえば次の処理を考えます。

注文イベント
   ↓
Lambda
   ↓
決済API
   ↓
注文ステータス更新

1回目のLambdaで決済が成功したあとに処理が失敗し、同じイベントが再試行されると、単純な実装では決済APIをもう一度呼ぶ可能性があります。

必要なのは「この注文はすでに処理したか」を判断する仕組みです。

冪等性キーを決める

重複判定の基準になる値を**冪等性キー(idempotency key)**と呼びます。

注文処理なら、たとえばorderIdを利用できます。

{
  "orderId": "order-123",
  "userId": "user-456",
  "amount": 5000,
  "requestedAt": "2026-09-23T10:00:00Z"
}

イベント全体を重複判定に使うと、requestedAtのように毎回変わる値のせいで別イベントと判定される場合があります。

そのため、ビジネス上「同じ処理」を表す安定した値をキーにするのが重要です。

Powertools for AWS Lambdaを使う

AWS Lambda Powertools for Javaには、冪等性を実装するためのユーティリティがあります。

基本的な流れは次の通りです。

リクエスト
   ↓
冪等性キーを生成
   ↓
DynamoDBで状態確認
   ├─ 未処理 → INPROGRESSを記録 → 本処理
   ├─ 処理中 → 重複実行を防止
   └─ 完了済み → 保存済み結果を返す

Java版では標準の永続化先としてDynamoDBを利用できます。

DynamoDBテーブルを用意する

標準設定では、次のような属性を利用します。

Partition key: id
TTL attribute: expiration

TTLはTime To Liveの略で、一定時間を過ぎた古いレコードをDynamoDBから自動削除するための仕組みです。

冪等性レコードを永久保存する必要がない場合に利用します。

AWS SAMなら概念的には次のようなテーブルになります。

IdempotencyTable:
  Type: AWS::DynamoDB::Table
  Properties:
    BillingMode: PAY_PER_REQUEST
    AttributeDefinitions:
      - AttributeName: id
        AttributeType: S
    KeySchema:
      - AttributeName: id
        KeyType: HASH
    TimeToLiveSpecification:
      AttributeName: expiration
      Enabled: true

PAY_PER_REQUESTはアクセス量に応じて課金されるDynamoDBのオンデマンドキャパシティモードです。小規模なLambdaで最初から読み書き性能を細かく指定したくない場合にも扱いやすい設定です。

Javaで設定する

Powertoolsの永続化ストアは、Lambdaハンドラーの実行ごとではなく、ハンドラー外で初期化します。

public class OrderHandler implements RequestHandler<OrderEvent, OrderResult> {

    public OrderHandler() {
        Idempotency.config()
            .withPersistenceStore(
                DynamoDBPersistenceStore.builder()
                    .withTableName(System.getenv("IDEMPOTENCY_TABLE"))
                    .build()
            )
            .configure();
    }

    @Idempotent
    @Override
    public OrderResult handleRequest(OrderEvent event, Context context) {
        return processOrder(event);
    }

    private OrderResult processOrder(OrderEvent event) {
        // 決済、DB更新など
        return new OrderResult(event.orderId(), "completed");
    }
}

@Idempotentを付けることで、Powertoolsが実行状態を永続化し、同一リクエストの重複処理を抑制します。

初期化をコンストラクタなどハンドラー外で行うのは、Lambdaの実行環境が再利用される場合に設定を毎回作り直さないためです。

イベント全体をキーにしない

実務ではここが重要です。

たとえば次の2イベントを考えます。

{
  "orderId": "order-123",
  "requestedAt": "10:00:00"
}
{
  "orderId": "order-123",
  "requestedAt": "10:00:05"
}

業務上は同じ注文でも、イベント全体をハッシュ化すると別物になる可能性があります。

PowertoolsではJMESPathを使ってイベントの一部を冪等性キーとして選択できます。

JMESPathはJSONから必要な値を抽出するためのクエリ言語です。

たとえばorderIdだけを重複判定に利用します。

Idempotency.config()
    .withPersistenceStore(
        DynamoDBPersistenceStore.builder()
            .withTableName(System.getenv("IDEMPOTENCY_TABLE"))
            .build()
    )
    .withConfig(
        IdempotencyConfig.builder()
            .withEventKeyJMESPath("orderId")
            .withThrowOnNoIdempotencyKey(true)
            .build()
    )
    .configure();

withThrowOnNoIdempotencyKey(true)を設定しておけば、キーが取得できないイベントをそのまま実行するのではなくエラーにできます。

「冪等性が必要な処理なのにキーがない」というデータ不整合を早めに検知できます。

同時に同じイベントが来たらどうなる?

単に「完了済みか」を確認するだけでは不十分です。

ほぼ同時に2つのLambdaが起動すると、両方が未処理と判断して処理を始める競合が起こり得るためです。

Powertoolsは処理開始時にINPROGRESS状態を記録します。同じペイロードの処理が進行中の場合、Java版ではIdempotencyAlreadyInProgressExceptionを使って同時実行を防ぎます。

つまり、

Lambda A → INPROGRESSを作成 → 決済処理
Lambda B → INPROGRESSを検出 → 本処理を実行しない

という形にできます。

これは単純な「処理後にIDを保存する」方式より安全です。

TTLは業務要件に合わせる

Powertools for AWS Lambda (Java) の現行ドキュメントでは、冪等性レコードの有効期間はデフォルトで3600秒(1時間)です。

ただし、1時間が正しいとは限りません。

たとえば注文IDが永久に再利用されない設計なら、より長い期間の重複防止が必要になるかもしれません。一方、短時間だけ再試行される処理なら長期間保持する必要はない場合があります。

IdempotencyConfig.builder()
    .withExpiration(Duration.of(24, ChronoUnit.HOURS))
    .build();

重要なのは「Lambdaの設定値」ではなく、「同じ操作を何時間後まで重複とみなすか」という業務要件から決めることです。

Lambdaタイムアウト時にも注意する

厄介なのは、処理途中でLambdaがタイムアウトしたケースです。

INPROGRESS作成
   ↓
外部API呼び出し
   ↓
Lambdaタイムアウト

INPROGRESSが永遠に残ると、以降の再試行を処理できません。

Java版PowertoolsではLambdaコンテキストの残り実行時間を冪等性レコードに反映し、タイムアウトした実行を次の再試行で再実行できるようにする仕組みがあります。

ただし、ここでも外部API側で処理が完了していた可能性は残ります。決済など重要な外部APIを呼ぶ場合は、可能なら外部API自身のidempotency keyも併用するのが安全です。

「Lambda全体を冪等」にすれば万能ではない

次のように1つのLambdaが複数の副作用を持つ場合があります。

1. 決済
2. PostgreSQL更新
3. メール送信

Lambda全体を1つの冪等処理にすると、途中失敗時の扱いが難しくなる場合があります。

AWSのドキュメントでも、ハンドラー内に複数の副作用がある場合は、処理を分割することを検討するよう案内されています。

たとえば、

注文受付
 ↓
決済Lambda
 ↓
イベント発行
 ↓
メールLambda

のように責務を分け、それぞれで適切な冪等性キーを持たせる方が状態を追いやすくなります。

SQSの部分バッチレスポンスとは別の問題

以前の記事で扱ったReportBatchItemFailuresは、「SQSバッチのうち失敗したメッセージだけを再試行する」ための仕組みです。

一方、冪等性は「同じメッセージが再び処理されても副作用を重複させない」ための仕組みです。

ReportBatchItemFailures
→ 再試行するメッセージを減らす

Idempotency
→ 再試行されても安全にする

役割が異なるため、SQS + Lambdaでは両方を組み合わせる設計も有効です。

実務で確認したいポイント

Lambdaで副作用のある処理を実装するときは、最低限次を確認しておくと安全です。

  • 同じイベントが2回来ても安全か
  • 冪等性キーは業務上の同一操作を正しく表しているか
  • タイムスタンプなど毎回変わる値をキーに含めていないか
  • 同時実行時にも二重処理を防げるか
  • 冪等性レコードの有効期間は業務要件に合っているか
  • 外部APIにもidempotency keyを渡せるか
  • 1つのLambdaに副作用を詰め込みすぎていないか

「再試行をなくす」のではなく、再試行されても壊れない処理にすることが重要です。

参考資料