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
- E-commerce Transactions: Ensuring that orders are not duplicated when a user accidentally submits the order button multiple times.
- Financial Services: Preventing multiple deposits or withdrawals from being processed more than once when there are network retries.
- 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.