Nordlet

← ドキュメント / 用語集

冪等性とは何ですか。会計および決済 API に冪等性が必要な理由

リクエストの再試行によって重複請求書が作成される仕組み、冪等性キーが保証する内容、処理すべき障害モード、および正しく実装する方法について解説します。

ある操作を 2 回実行した結果が、1 回実行した場合と同じになる場合、その操作は冪等(いであとう)であると言います。レコードの読み取りは本来的に冪等です。一方、請求書の作成はそうではありません。これを 2 回呼び出すと、2 通りの請求書と 2 通りの売掛金が生まれ、顧客から「なぜ 2 重に請求されたのか」と問い合わせを受けることになります。

このギャップが問題になる理由は、ネットワークが特異かつ厄介な障害を起こすためです。リクエストは成功したものの、レスポンスが途中で失われる場合があります。クライアント側からはタイムアウトとして認識されるため、「一度も届かなかったのか」、あるいは「届いて処理されたが返信が失われたのか」を判別できません。この状況で安全に見える唯一の選択肢は再試行ですが、重複を生み出してしまうのはまさにこの再試行なのです。

会計システムにおいて状況がさらに悪化する理由

ほとんどのシステムにおいて、重複は単なる面倒な事象に過ぎません。しかし、台帳(ledger)システムにおいては複利的な問題へと発展します。

  • 重複した請求書は連続する番号から 1 つ番号を消費するため、単に削除することはできません。不変性(immutability)の原則により、修正にはクレジットノート(貸方票)が必要となります。これは顧客の目に触れる文書です。
  • 重複分が仕訳として計上されるため、誰かが気づくまでの間、売上高、VAT、売掛金のすべてが過大計上された状態になります。
  • VAT 申告(vat-return)や、リトアニアにおける i.SAF 登録にもその重複データが取り込まれます。
  • 決済がトリガーされていた場合、実際に資金が移動してしまいます。

また、「単に重複チェックを行う」というアプローチもうまくいきません。同じ顧客に対し、同じ金額の請求書を同じ日に、全く別の目的で 2 通発行することは日常茶飯事だからです。システムは内容から意図を推測することができないため、クライアント自身がそれを明示する必要があります。

冪等性キーの仕組み

クライアントは論理的な操作ごとに一意のキーを生成し、リクエストと共に送信します。

POST /v1/sales/invoices/create
Idempotency-Key: 8f14e45f-ea1b-4c2b-9f3a-2d7c1e0b5a91

これにより、サーバー側は以下の動作を保証します。

  1. このキーによる初回のリクエスト - 通常通り処理し、その結果をキーに紐づけて保存します。
  2. 同じキーと同じペイロードによる再試行 - 再実行は行わず、保存されたレスポンスを返します。
  3. 同じキーだがペイロードが異なる場合 - 拒否します。キーは「これが同一の操作である」という主張を示しているため、ボディが異なる場合は同一操作ではありません。古い結果を暗黙的に返すことは、エラーを返すよりも悪影響を及ぼします。
  4. 元の処理がまだ実行中の段階での再試行 - 同時に実行するのではなく、再試行可能なエラーとして拒否します。

4番目のポイントは、素朴な実装で見落とされがちな点です。5 秒でタイムアウトし 6 秒目に再試行を行うクライアントは、自身の処理中(in-flight)のリクエストと競合します。並行処理のガードがなければ両方が実行されてしまい、まさにこの仕組みが防ごうとしていた重複が発生します。

キーを正しく生成する方法

キーは試行回数ではなく、操作そのものを識別するものでなければなりません。

  • 正しい例: ユーザーが「請求書を発行する」をクリックしたときに一度だけ生成され、そのクリックのすべての再試行で再利用される UUID。
  • 正しい例: 自社のドメインから導出された決定論的なキー (order-1041-invoice)。
  • 誤った例: HTTP の試行ごとに新しく作成される UUID。すべての再試行で新しいキーが割り当てられるため、すべての再試行が新しい操作とみなされ、何も実装していないことになります。
  • 誤った例: ペイロードのハッシュ。これでは完全に同一の正当な 2 つの操作が競合し、2 つ目の操作が暗黙的に無視されてしまいます。

キーには保持期間が必要です。現実的な再試行の猶予期間やクライアントの障害をカバーできる長さであるべきですが、永久に保持する必要はありません。

冪等性が解決しない問題

冪等性は、同じリクエストの重複実行を防ぎます。以下のような問題を防ぐことはできません。

  • 異なるセッションから 2 人の異なるユーザーが同じ請求書を作成した場合。これはビジネス上のルールに関する問題であり、「1 注文につき 1 請求書」といった一意性制約によって解決されます。
  • 異なるキーを持つ論理的な重複。
  • 操作内部の非決定性。ハンドラーがタイムスタンプや番号を生成する場合、保存されたレスポンスには最初の値がキャプチャされます。これは望ましい動作ですが、理解しておく価値があります。

冪等性はデータベース制約を置き換えるものではなく、補完し合うものです。Nordlet の台帳では両方を使用しています。冪等性キーによって再試行されたリクエストを阻止しつつ、一意性インデックスによって、ペイアウト ID ごとの決済バッチの重複、サプライヤーおよび伝票番号ごとの仕入請求書の重複、そして資産および期間ごとの減価償却実行の重複を防ぎます。

Nordlet における実装方法

状態を変更する任意のリクエストに対して、空ではない最大 255 文字の文字列である Idempotency-Key ヘッダーを送信します。

指定されたキーによる初回のリクエストが実行され、その結果が保存されます。同じキーとペイロードによる再試行では、再実行することなく保存されたレスポンスが返されるため、呼び出しが何回繰り返されても請求書は正確に 1 回だけ作成されます。

2 つの障害モードについては、暗黙的に処理されるのではなく、明示的に拒否されます。

  • 同じキーでペイロードが異なる場合 → 拒否されます: "Idempotency-Key was already used with a different request payload." これにより、クライアントが金額を変更したにもかかわらずキーを再利用してしまったという、本質的に危険なケースを検知できます。
  • 同じキーで元の処理がまだ実行中の場合 → 再試行可能なエラーとして拒否されます: "A request with this Idempotency-Key is still being processed, retry shortly." 同時並行での二重実行を防ぎます。

キーは企業(company)単位でスコープが設定されるため、2 つのテナントが同じ UUID を生成しても衝突することはありません。

これと補完関係にある保証が逆方向にも働きます。**Webhook の配信は少なくとも 1 回(at-least-once)**行われます。つまり、実際には配信が成功したにもかかわらずタイムアウトが発生した場合や、再配信が行われた場合などに、エンドポイントが同じイベントを偶発的に 2 回受信することがあります。すべてのイベントには ID が付与されているため、ハンドラー側で処理済み ID を記録し、重複分を無視するように実装する必要があります。冪等性は統合の両側で必要となる特性であり、リクエスト側については API 規約ガイドで詳しく解説しています。

FAQ

冪等性キーとは何ですか。

リクエストとともに送信される、クライアント側で生成される識別子です。サーバー側で同じ論理的操作の再試行であることを認識できるようにします。初回のリクエストが実行され、同じキーによる再試行では再実行の代わりに保存された結果が返されます。

会計 API に冪等性が必要な理由は何ですか。

重複した書き込みは、実際の効果を持つ実際の文書を生み出してしまうためです。具体的には、連続番号を消費する 2 つ目の請求書が台帳に計上され、VAT 申告に取り込まれてしまいます。重複した読み取りとは異なり、単に削除することはできず、クレジットノート(貸方票)で修正する必要があります。

再試行ごとに新しいキーを生成すべきですか。

いいえ。それでは仕組みの意味が完全に失われてしまいます。論理的な操作ごとに 1 つのキーを生成し、その操作のすべての再試行で再利用してください。試行ごとに新しいキーを使用すると、すべての試行が新しい操作として扱われてしまいます。

異なるペイロードでキーを再利用するとどうなりますか。

リクエストは拒否されます。キーはそれが同一の操作であることを主張しているため、ボディが変更されているということは何らかの問題(多くの場合、クライアント側でのキーの再利用バグ)が発生していることを意味します。以前の結果を暗黙的に返すことは、それよりも悪影響を及ぼします。

冪等性はすべての重複を防ぎますか。

いいえ。同じリクエストの重複実行を防ぎます。異なる 2 人のユーザーが独立して同じ請求書を作成した場合、それは 2 つの異なるリクエストであり、冪等性キーには検知できない一意性制約(ビジネスルール)に関する問題となります。