ネットショッピングで「購入」ボタンを2回押してしまったら、商品が2つ届いてしまうのでしょうか。答えは、システムが 冪等性(べきとうせい) を備えているかどうかで決まります。

冪等性とは、同じ操作を何度実行しても、システムの状態が1回実行したときと変わらない性質です。英語では idempotency(アイデムポテンシー)と表記します。もともと数学の概念ですが、API設計・分散システム・データベースなど幅広い分野で欠かせない設計原則となっています。

冪等性の定義 — 数学からソフトウェアへ

数学における冪等性

数学では、ある演算 f に対して次の条件を満たすとき「冪等である」と定義します。

f(f(x)) = f(x)

身近な例を挙げます。

  • 絶対値関数 abs(-5) = 5abs(abs(-5)) = abs(5) = 5 → 何度適用しても結果は同じ
  • 集合の和集合 A ∪ A = A → 同じ集合を何度結合しても変化しない
  • ブール演算のOR true OR true = true → 繰り返しても値は変わらない

一方、f(x) = x + 1 は冪等ではありません。f(1) = 2f(f(1)) = f(2) = 3 のように適用するたびに値が変化するためです。

ソフトウェアにおける冪等性

ソフトウェアの文脈では、定義が少し広がります。

ある操作を1回実行した後の「システムの状態」と、同じ操作を複数回実行した後の「システムの状態」が同一であること

ここで重要なのは「状態」に着目している点です。レスポンスやステータスコードは異なる場合があります。たとえば、あるリソースに対してDELETEリクエストを初回送信すると 200 OK が返りますが、2回目は 404 Not Found が返ります。レスポンスは変化していますが、「リソースが存在しない」というサーバーの状態は1回目の実行後と同じです。これは冪等な操作です。

日常の例で理解する

冪等性は身近な場面にも存在します。

操作冪等?理由
エレベーターの「5階」ボタンを押すはい何度押しても5階に向かう状態は変わらない
横断歩道の歩行者ボタンを押すはい連打しても「青にする」リクエストは1回分として処理される
自動販売機に硬貨を投入するいいえ入れるたびに投入金額が加算される
音楽プレーヤーの「停止」ボタンを押すはい停止中に再度押しても停止のまま

HTTPメソッドと冪等性 — REST API設計の基本

REST APIを設計するうえで、各HTTPメソッドが冪等かどうかを把握しておくことは基本中の基本です。

一覧表

HTTPメソッド冪等安全説明
GETはいはいリソースの取得。サーバー状態を変更しない
HEADはいはいGETと同じだがボディなし。ヘッダー情報のみ取得する
PUTはいいいえリソース全体の置換。同じデータで何度PUTしても結果は同一
DELETEはいいいえリソースの削除。削除済みリソースを再度削除しても状態は変わらない
OPTIONSはいはい対応メソッドの問い合わせ
POSTいいえいいえリソースの新規作成。実行のたびに新しいリソースが生まれる
PATCH場合によるいいえリソースの部分更新。操作内容により冪等にも非冪等にもなる

※「安全」とは、サーバーの状態を変更しないことを意味します。安全なメソッドはすべて冪等ですが、冪等なメソッドがすべて安全とは限りません(PUT、DELETEは冪等だが安全ではない)。

PATCHが「場合による」理由

PATCHの冪等性は操作の書き方に依存します。

// 冪等なPATCH: 値を固定値で上書き
PATCH /products/1
{ "price": 1500 }
// → 何度実行しても price は 1500

// 非冪等なPATCH: 相対的な変更
PATCH /products/1
{ "price_increment": 100 }
// → 実行するたびに price が +100 される

PATCHを冪等にするには、相対的な変更(増減)ではなく絶対値で指定する設計にします。

POSTを冪等にする工夫 — Idempotency Key

POSTは本来非冪等ですが、Idempotency Key(冪等性キー)を導入することで冪等にできます。

POST /payments
Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890

{ "amount": 3000, "currency": "JPY" }

サーバー側はこのキーをデータベースに記録し、同じキーのリクエストが再度送られた場合は処理を実行せず、初回のレスポンスをそのまま返します。

決済API大手のStripeをはじめ、多くの決済サービスがこの方式を採用しています。

なぜ冪等性が重要なのか — 分散システムの現実

ネットワークは信頼できない

分散システムではネットワーク障害が避けられません。クライアントがリクエストを送信したあと、次のいずれの段階でも失敗が発生し得ます。

ネットワーク障害と冪等性:リクエスト未到達・処理中クラッシュ・レスポンス未到達の3つの失敗ポイントを示す図

③のケースが厄介です。サーバーでは処理が完了しているのに、クライアントにはタイムアウトとして見えます。クライアントが同じリクエストをリトライしたとき、操作が冪等でなければ二重処理が発生します。

At-Least-Once デリバリーと冪等性

AWS Lambda、Google Cloud Functions、Azure Functionsなどのサーバーレスプラットフォームや、Apache Kafka、Amazon SQSなどのメッセージキューは、メッセージの配信保証として At-Least-Once(少なくとも1回) を採用しています。

「少なくとも1回」は「最大1回(At-Most-Once)」よりメッセージ消失のリスクが低いですが、同じメッセージが2回以上配信される可能性があります。AWS公式ドキュメントでも、Lambdaの非同期呼び出しでは「少なくとも1回」実行されることが明記されており、まれに同じイベントが複数回処理されるケースが発生します(出典: AWS Lambda Idempotency)。

冪等性が保証されていれば、重複配信があっても「同じ処理が2回走るだけで、システムの状態には影響しない」ため安全です。

冪等性の実装パターン

パターン1: 一意キーによる重複排除(入力時チェック)

最もポピュラーな手法です。リクエストごとに一意なキー(UUID等)を付与し、処理済みかどうかをデータベースで確認してから実行します。

-- 冪等性キーを記録するテーブル
CREATE TABLE idempotency_keys (
    key       VARCHAR(255) PRIMARY KEY,
    response  JSONB,
    created_at TIMESTAMP DEFAULT NOW()
);

処理の流れは以下のとおりです。

  1. クライアントがリクエストに一意キーを付与して送信
  2. サーバーは idempotency_keys テーブルを検索
  3. キーが存在する場合: 保存済みレスポンスをそのまま返却
  4. キーが存在しない場合: 処理を実行し、結果をキーとともに保存
def process_payment(idempotency_key, payment_data):
    # 既存レコードを検索
    existing = db.query(
        "SELECT response FROM idempotency_keys WHERE key = %s",
        [idempotency_key]
    )
    if existing:
        return existing.response  # 保存済みレスポンスを返却

    # 処理を実行
    result = execute_payment(payment_data)

    # 結果を保存(原子的に実行する)
    db.execute(
        "INSERT INTO idempotency_keys (key, response) VALUES (%s, %s)",
        [idempotency_key, json.dumps(result)]
    )
    return result

注意点: 処理の実行とキーの保存は同一トランザクション内で行う必要があります。そうしないと、処理は成功したがキー保存に失敗し、リトライ時に二重処理が発生するリスクがあります。

パターン2: UPSERT(存在すれば更新、なければ挿入)

データベースのUPSERT機能を使い、操作自体を冪等にする方法です。

-- PostgreSQLの場合
INSERT INTO user_settings (user_id, theme, language)
VALUES (42, 'dark', 'ja')
ON CONFLICT (user_id)
DO UPDATE SET theme = EXCLUDED.theme, language = EXCLUDED.language;

このSQL文は何度実行しても、user_id = 42 の設定が theme = 'dark', language = 'ja' になるという同じ結果を生みます。

パターン3: 条件付き更新(楽観的ロック)

バージョン番号やタイムスタンプを使い、期待する状態の場合のみ更新を適用します。

UPDATE orders
SET status = 'shipped', version = version + 1
WHERE order_id = 'ORD-001' AND version = 3;

version = 3 の場合のみ更新が実行されます。2回目以降の同じリクエストではバージョンが4に変わっているため、更新は空振りします。

パターン4: ステートマシンによる状態遷移制御

決済システムなどでは、処理の状態をステートマシンで管理し、許可された遷移のみを受け付ける方法が効果的です。

[pending] → [processing] → [completed]
           [failed] → [pending]  (リトライ)

たとえば、すでに completed 状態の注文に対して再度「完了にする」リクエストが来ても、ステートマシンが遷移を拒否するため二重処理が発生しません。米国の決済プラットフォームModern Treasuryでは、このステートマシン方式とIdempotency Key方式を二重に組み合わせ、内部・外部の両面から冪等性を保証しています。

身近なサービスでの冪等性 — 決済APIの実例

Stripe APIは、冪等性キーの実装例としてよく知られています。

curl https://api.stripe.com/v1/charges \
  -u sk_test_xxx: \
  -H "Idempotency-Key: unique-key-12345" \
  -d amount=2000 \
  -d currency=jpy

Stripeでは冪等性キーをAPI v1で24時間、API v2で30日間保持します(出典: Stripe公式ドキュメント)。同じキーで期限内にリクエストを再送すると、初回のレスポンスが返却されます。この有効期限の設計は「一時的なネットワーク障害への対処」を目的としており、恒久的な重複排除には別の仕組み(注文IDベースのビジネスロジック)を使うべきだという思想に基づいています。

PRGパターン — Webフォームの二重送信を防ぐ

Webアプリケーションで「注文確定」フォームを送信後にブラウザのリロードボタンを押すと、同じPOSTリクエストが再送される問題は古くから知られています。これを防ぐ定番手法がPRG(Post/Redirect/Get)パターンです。

PRGパターン(Post/Redirect/Get):POSTで注文送信→303リダイレクト→GETで完了ページ表示の流れ。リロードしてもGETが再送されるだけで安全

POSTの処理後にHTTP 303リダイレクトでGETに切り替えることで、ブラウザの履歴にはGETリクエストだけが残ります。リロードしてもGET(冪等な操作)が再送されるだけなので、注文の二重送信は発生しません。

Infrastructure as Code(IaC)での冪等性

Terraform、Ansible、AWS CloudFormationなどのIaCツールにおいて、冪等性は根幹をなす概念です。

宣言型 vs 手続き型

アプローチ冪等性動作
宣言型自然に冪等「Webサーバーを3台にする」現在2台なら1台追加、すでに3台なら何もしない
手続き型冪等でない場合が多い「Webサーバーを1台追加する」実行するたびに1台ずつ増える

Terraformは宣言型のアプローチを採っており、terraform apply を何度実行しても、定義ファイルに書かれた状態に収束します。現在の状態と宣言された状態の差分だけを適用する仕組みのため、冪等性が構造的に保証されています。

メッセージキューと冪等性

Apache KafkaやAmazon SQSなどのメッセージキューでは、コンシューマー側で冪等性を確保する設計が求められます。

重複排除の実装例

def consume_message(message):
    transaction_id = message["transaction_id"]

    # 処理済みかチェック
    if db.exists("processed_messages", transaction_id):
        return  # すでに処理済み — スキップ

    # ビジネスロジックを実行(原子的トランザクション内で)
    with db.transaction():
        execute_business_logic(message)
        db.insert("processed_messages", {
            "transaction_id": transaction_id,
            "processed_at": datetime.now()
        })

    # ACK(確認応答)を送信
    queue.ack(message)

処理の実行・処理済みIDの記録・ACKの送信を正しい順序で行うことが重要です。処理が完了する前にACKを送ると、処理途中でクラッシュした場合にメッセージが消失します。逆に、処理完了後ACK送信前にクラッシュすると、メッセージが再配信されるため冪等性が必要になります。

データベース操作の冪等性

データベース操作の種類によって冪等性は異なります。

操作冪等理由
SELECTはいデータを読み取るだけで状態を変更しない
INSERT(キー重複なし)いいえ同じINSERTを2回実行すると2行挿入される
INSERT … ON CONFLICTはいキー重複時はUPDATEまたは無視される
UPDATE SET x = 5はい何度実行しても x は 5 のまま
UPDATE SET x = x + 1いいえ実行のたびに x が増加する
DELETE WHERE id = 1はい2回目以降は削除対象がないだけ

設計指針として、絶対値での指定(SET x = 5)を優先し、相対値での変更(SET x = x + 1)は避けることで冪等性を保ちやすくなります。

冪等性が保証できない場面と対処法

冪等性には限界があり、すべての操作を冪等にできるわけではありません。

本質的に非冪等な操作

  • メール・SMS送信: 同じ内容を2回送ると受信者に2通届く
  • 外部API呼び出し: 相手のシステムが冪等性を保証しているとは限らない
  • ログ出力: 同じ操作でもログは蓄積される(ただし副作用として許容されることが多い)

対処パターン

1. At-Most-Once(最大1回)実行

リトライを無効化し、1回だけ実行する方式です。失敗時はエラーを記録し、別の手段(手動対応、補償処理)で回復します。

2. 検証による冪等性(Idempotency by Validation)

非冪等な操作を実行した後、冪等な読み取り操作で結果を確認します。成功していればリトライ不要、失敗していれば再試行するという手法です。

def send_notification(user_id, message_id):
    # まず送信済みかチェック(読み取り = 冪等)
    if notification_service.is_delivered(message_id):
        return  # すでに送信済み

    # 送信を試みる(書き込み = 非冪等)
    notification_service.send(user_id, message_id)

3. 補償トランザクション

操作が二重実行された場合に、その影響を打ち消す逆操作を用意しておく方法です。ECサイトの返品処理や、決済のキャンセル処理がこれに該当します。

冪等性とステートレスの違い

冪等性とステートレスは混同されがちですが、異なる概念です。

概念意味
冪等性同じ操作を繰り返しても状態が変わらないPUT /users/1 {"name": "田中"}
ステートレスサーバーが過去のリクエスト情報を保持しないRESTful APIの各リクエストが独立

ステートレスな設計は冪等性を実現しやすくしますが、ステートレスであることは冪等性を保証しません。たとえば、ステートレスなAPIでも POST /items を2回実行すればアイテムが2つ作成される可能性があります。

逆に、ステートフルな仕組み(データベースに処理済みキーを保存する等)を活用することで冪等性を実現するケースも多くあります。

冪等設計のベストプラクティス

冪等なシステムを設計するためのポイントをまとめます。

1. 一意な識別子を活用する

すべてのリクエストにUUIDやトランザクションIDを付与し、重複を検出できるようにします。Idempotency Keyの生成はできるだけ**リクエストの発生元(フロントエンド)**で行うのが望ましいです。カート作成時にキーを生成してチェックアウトリクエストに含めれば、ブラウザの二重送信もカバーできます。

2. 絶対値指定を優先する

SET balance = 1000 のように最終的な状態を指定し、SET balance = balance - 500 のような相対変更は避けます。相対変更が必要な場合はトランザクションIDと組み合わせて重複排除します。

3. 処理とキー記録を原子的に行う

冪等性キーの保存とビジネスロジックの実行を同一トランザクションにまとめます。いずれか一方だけが成功する中間状態を防ぎます。

4. 副作用を分離する

メール送信やログ出力などの副作用は、冪等な主処理から分離して非同期で実行します。副作用自体にも重複排除の仕組み(送信済みフラグ等)を設けます。

5. レスポンスをキャッシュする

冪等性キーに紐づくレスポンスを保存しておき、再リクエスト時には処理を再実行せずキャッシュされたレスポンスを返却します。これによりパフォーマンスの劣化も防げます。

6. 有効期限を設定する

冪等性キーのレコードは永続的に保持する必要はありません。Stripeのように24時間~30日間など、ユースケースに応じた有効期限を設定します。古いレコードを定期的にクリーンアップすることでストレージ消費を抑えます。

まとめ — 冪等性はシステムの信頼性を支える設計原則

冪等性の本質は「何度実行しても安全」という保証をシステムに組み込むことです。

  • 定義: 同じ操作を複数回実行しても、システムの状態が1回実行時と同一であること
  • REST API: GET / PUT / DELETE は冪等、POSTは非冪等(Idempotency Keyで冪等化可能)
  • 分散システム: At-Least-Once配信環境では冪等性が必須
  • 実装パターン: 一意キーによる重複排除、UPSERT、条件付き更新、ステートマシンの4パターン
  • 限界: メール送信など本質的に非冪等な操作には、At-Most-Once実行や補償トランザクションで対処する

ネットワーク障害やリトライ処理が日常的に発生する現代のシステムにおいて、冪等性は「あると便利」ではなく「ないと困る」設計原則です。API設計の初期段階から冪等性を意識し、各操作に適切なパターンを適用することが、信頼性の高いシステム構築につながります。