Idempotency ensures that multiple identical requests have the same effect as making a single request. This pattern addresses how to design HTTP POST methods to be idempotent, which is crucial in avoiding unintended side effects in API operations, especially in scenarios involving retries or duplicate requests.

Details

HTTP POST methods are traditionally non-idempotent, meaning that repeating the same POST request can lead to multiple instances of the same resource being created. This can cause issues like duplicate transactions or entries, which are undesirable in most business scenarios.

To make POST methods idempotent, the implementation typically involves assigning a unique identifier to each request, known as a client-generated identifier (CGI). This identifier is used to detect and prevent duplicate operations from being executed.

Alternatively, implementations may check for an existing entry in the underlying data store using a single identifier or multiple indentifiers that uniquely identify an entry. This will prevent duplicates from being created.

When an existing resource exists already, the API may return a 200 OK rather than a 201 Created to ensure the client is aware of the resource.

Common Pattern Names/Synonyms

  • Idempotent POST
  • Replay-safe POST
  • Safe repeat POST

Common Use Cases

  1. E-commerce Transactions: Ensuring that orders are not duplicated when a user accidentally submits the order button multiple times.
  2. Financial Services: Preventing multiple deposits or withdrawals from being processed more than once when there are network retries.
  3. Registration Systems: Avoiding multiple registrations from the same user due to retries or resubmissions.

Mermaid Sequence Diagrams

Basic Idempotent POST Processing

sequenceDiagram
    participant Client
    participant Server
    Client->>Server: POST /resource {data, id="xyz123"}
    alt Resource Exists with id="xyz123"
        Server->>Client: HTTP 200 OK {resource details}
    else Resource Does Not Exist
        Server-->>Server: Create resource
        Server->>Client: HTTP 201 Created {resource details}
    end

Handling Duplicate Requests

sequenceDiagram
    participant Client
    participant Server
    Client->>Server: POST /resource {data, id="xyz123"}
    Server->>Server: Check if id="xyz123" exists
    alt id exists
        Server->>Client: HTTP 200 OK {existing resource details}
    else id does not exist
        Server-->>Server: Create resource with id="xyz123"
        Server->>Client: HTTP 201 Created {new resource details}
    end

Examples

Example 1: Client-Provided Identifier

Request

POST /orders HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "product_id": 112233,
  "quantity": 1,
  "client_id": "order12345xyz"
}

Server-side Implementation

The following Python example looks for an existing client-provided identifier that is stored on the server and used to detect if this is a duplicate request.

from flask import Flask, request, jsonify
app = Flask(__name__)
orders = {}

@app.route('/orders', methods=['POST'])
def create_order():
    data = request.json
    client_id = data['client_id']
    # Check for a pre-existing resource using the client-provided identifier
    if client_id in orders:
        return jsonify(orders[client_id]), 200
    else:
        # Assume order creation logic here
        orders[client_id] = data
        return jsonify(data), 201

if __name__ == '__main__':
    app.run()

Example 2: Creating User with Unique Email

Request

POST /users HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "email": "example@example.com",
  "name": "John Doe",
  "client_id": "unique12345"
}

Server-side Implementation

The following Python example looks for an existing user by checking for the email prior to creating a new user. If the user already exists, it is returned with a 200 OK response.

from flask import Flask, request, jsonify
app = Flask(__name__)
users = {}

@app.route('/users', methods=['POST'])
def create_user():
    data = request.json
    client_id = data['client_id']
    if client_id in users:
        return jsonify(users[client_id]), 200
    else:
        # Prevent duplicate user creation based on unique email
        if any(user['email'] == data['email'] for user in users.values()):
            return jsonify({"error": "Email already exists"}), 400
        # No duplicate found, so create the user and return a 201 Created with the user in the response.
        users[client_id] = data
        return jsonify(data), 201

if __name__ == '__main__':
    app.run()

This approach, using client-generated identifiers, ensures that POST requests are idempotent by utilizing a unique reference to detect and handle repeat submissions effectively.

Updated: