1.3.1 Idempotent
Idempotent means:
lightbulbYou can perform the same operation multiple times, and after the first successful operation, the final result remains the same.
Simple example
Imagine a light switch:
Set light = ON
Do it once:
OFF → ON
Do it again:
ON → ON
Do it 10 times:
ON → ON → ON → ON ...
The final state is still ON. Therefore, set light = ON is idempotent.
In APIs
This is especially important in REST APIs.
| HTTP method | Usually idempotent? | Example |
|---|---|---|
GET | ✅ Yes | Get user 123 |
PUT | ✅ Yes | Set user 123's name to "Ali" |
DELETE | ✅ Yes | Delete user 123 |
POST | ❌ Usually no | Create a new user |
For example:
PUT /users/123
{
"name": "Ali"
}
Send it once:
User 123 → name = Ali
Send the exact same request again:
User 123 → name = Ali
Send it 100 times:
User 123 → name = Ali
It doesn't create 100 users. That's idempotency.
Why it matters in real systems
Suppose your frontend sends:
POST /payments
The network fails and the frontend doesn't know whether the payment succeeded.
If it retries, you could accidentally charge the customer twice.
An idempotency key solves this:
POST /payments
Idempotency-Key: abc123
If the same request with abc123 arrives again, the backend recognizes it as the same operation and doesn't perform the payment again.
Important distinction
Idempotent does NOT mean "the request can only be sent once."
It means:
lightbulbRepeating the same operation produces the same intended final state/effect.
For backend/API development, this concept becomes particularly important with payments, order creation, background jobs, retries, and distributed systems.
Since you're working on the Elara Training Platform, here's a very practical example.
Scenario: Assign Training Modules to a Teacher
Admin clicks:
POST /api/v1/integration/users/assign-modules
Request:
{
"email": "teacher@school.com",
"school_name": "Oak Primary School",
"module_ids": ["module-1", "module-2"]
}
Backend:
- Create teacher (if not exists)
- Assign Module 1
- Assign Module 2
Problem Without Idempotency
Suppose Umaisa sends the request.
Network times out.
Umaisa thinks:
Maybe Elara didn't receive it.
So it sends the same request again.
Now your database may contain:
Teacher: teacher@school.com
Assignments:
- Module 1
- Module 2
- Module 1 ← duplicate
- Module 2 ← duplicate
Or you might send duplicate emails:
Welcome email #1
Welcome email #2
Idempotent Solution
Before creating assignments, check whether they already exist.
Assignment Table
teacher_id | module_id
-----------+----------
1 | module-1
1 | module-2
Add a unique constraint:
UNIQUE (teacher_id, module_id)
Now when the same request arrives again:
teacher_id=1
module_id=module-1
Database says:
Already exists.
No duplicate assignment is created.
FastAPI Example
for module_id in module_ids:
existing = await session.scalar(
select(UserModule)
.where(
UserModule.user_id == user.id,
UserModule.module_id == module_id
)
)
if existing:
continue
session.add(
UserModule(
user_id=user.id,
module_id=module_id
)
)
First Call
Assign Module 1 ✅
Assign Module 2 ✅
Second Call (same request)
Module 1 already assigned → skip
Module 2 already assigned → skip
Final state:
Teacher has:
- Module 1
- Module 2
Exactly the same as after the first request.
Another Elara Example: Mark Module Complete
Endpoint:
POST /modules/1/complete
Bad implementation:
teacher.completed_modules += 1
If frontend accidentally sends the request 3 times:
Completed count:
1
2
3
Wrong.
Better:
Store completion as a state.
module_progress.completed = True
Even if called 10 times:
True
True
True
True
Final state remains:
Completed = True
That's idempotent.
Rule of Thumb
For your school/training platform:
✅ Assign module to teacher ✅ Enroll teacher in programme ✅ Mark module complete ✅ Update profile ✅ Activate account
These should usually be idempotent.
⚠️ Send email ⚠️ Create payment ⚠️ Create audit log entry
These are often not idempotent unless you add an idempotency key or deduplication logic.
Practical Example: Payment API
Suppose you have an e-commerce website.
User clicks "Pay Now".
Frontend sends:
POST /payments
Backend:
- Charges customer
- Creates payment record
- Returns success
Problem
Imagine this happens:
Frontend ---> Backend
Charge successful ✅
Response lost ❌
The frontend never receives the success response because of a network issue.
It thinks:
Maybe payment failed.
Let's try again.
So it sends:
POST /payments
again.
Now customer gets charged twice.
Bad Implementation
@app.post("/payments")
async def create_payment(payment: PaymentCreate):
charge_customer(payment.amount)
db_payment = Payment(amount=payment.amount)
session.add(db_payment)
session.commit()
return {"success": True}
If the request arrives twice:
Request 1 -> Charge $100
Request 2 -> Charge $100 again
Customer pays $200.
Solution: Idempotency Key
Frontend generates a unique UUID:
import uuid
idempotency_key = str(uuid.uuid4())
Example:
c8b5e5d0-9f2c-4fd4-a9e2-123456789abc
Request:
POST /payments
Idempotency-Key: c8b5e5d0-9f2c-4fd4-a9e2-123456789abc
Backend Logic
Before processing:
existing = db.query(Payment).filter(
Payment.idempotency_key == key
).first()
if existing:
return existing
If not found:
charge_customer()
payment = Payment(
amount=100,
idempotency_key=key
)
db.add(payment)
db.commit()
What Happens Now?
First Request
Key = abc123
Database:
No record found
Process payment:
Charge customer ✅
Save key abc123 ✅
Retry Request
Key = abc123
Database:
Record already exists
Backend:
Do NOT charge again
Return previous response
Result:
Customer charged only once ✅
Another Practical Example: User Registration
Bad:
POST /users
Body:
{
"email": "ali@gmail.com"
}
User clicks submit 3 times.
Result:
Ali #1
Ali #2
Ali #3
Three users created.
Solution
Use a unique constraint:
UNIQUE(email)
Then:
existing_user = get_user_by_email(email)
if existing_user:
return existing_user
or let the database reject duplicates.
In Your FastAPI Projects
You will commonly use idempotency for:
- Payments
- Module assignment APIs
- Invite user APIs
- Email sending
- Background jobs
- Webhook processing (Stripe, PayPal, etc.)
A common production pattern is:
Client
|
| POST + Idempotency-Key
v
FastAPI
|
| Check Redis/Postgres
|
+--> Key exists -> Return old response
|
+--> Key missing -> Process request
|
v
Store result
This is how systems handle retries safely when networks, browsers, mobile apps, or load balancers resend the same request.