In the context of development, idempotence means that calling a certain function once or multiple times should have the same effect on the system's state.
Being idempotent does not mean the return value is always the same, but that the function, if it has changed the system state, does not make any further changes if it is immediately called again with the same parameters.
So, for example, a function that returns the balance of an account is idempotent, even if it returns different values, because other functions may have changed the system state between one call and another.
Idempotence in our APIs
When using our APIs, to ensure idempotency in calls, we use the pattern known as Idempotence-Key
, which means that when making a call to our API, it is important to specify an ID that your system understands as corresponding to the operation.
The way we implement Idempotence-Key in our system is through the a property named "correlationID" inside the body.
For example:
You want to use our API to create a charge
In your system, you should have a table/collection that represents the creation of the charge, create a document and get its ID
In the call to our API, add a correlationID property with that ID.
When using our webhooks, it is necessary to ensure that the route that handles the webhooks is idempotent, as it is common practice to resend webhooks to ensure that at least one of them has arrived and been processed. At the end of this document, there are examples of how you can implement this.
Why is idempotence relevant?
A recurring problem in event-driven systems is duplicating events, which can occur for many reasons, such as communication failures, parallel invocations of the functions that generate the events, etc.
Resilient systems are built taking into consideration that these problems can and will frequently happen, and preparing adequately so that communication failures do not cause a bad internal state.
So, the main importance of idempotence is to make your system resilient and reliable.
When should a function be idempotent?
As with everything in development, it heavily depends on the project goal and business rules.
However, there are some patterns:
REST APIs
PUT routes
All routes with the PUT verb, which make changes to entities.
For example: A route that serves to change the address of a client.
As long as the new address is the same, it shouldn't matter to the system state how many times that route is called.
GET routes
All routes with the GET verb, which are meant to read information.
For example: A route that serves to read the current balance of a bank account.
Even if the balance changes over time, it is safe to call this route multiple times without causing changes to the system state.
DELETE routes
All routes with the DELETE verb, which are meant to delete entities.
For example: A route that serves to delete a charge.
Once called, the function will delete the bank account, subsequent calls should not delete other accounts, but return a message saying that this account has already been deleted.
Implementing Idempotence
These are some patterns for implementing idempotence:
GetOrCreate Pattern
The idea behind this pattern is to check if the entity that the function is trying to create already has an equivalent created in the database.
Assuming this function:
const createCustomerBankAccount = function(customer, params){
// Model validation
...
// Creating the account in the database
db.customerBankAccount.create({ ...params, customer: customer.id })
}
Applying this pattern, it can be rewritten as follows:
const getOrCreateCustomerBankAccount = function(customer, params){
// Before making creation validations, we can look for the most important parameters in our database and return an already existing account
const existingCustomerBankAccount = db.customerBankAccount.get({
customer: customer.id,
bankId: params.bankId
})
if (existingCustomerBankAccount){
return existingCustomerBankAccount;
}
// Model validation
...
// Creating the account in the database
db.customerBankAccount.create({ ...params, customer: customer.id })
}
Call History Pattern
The idea behind this pattern is to save all calls made to the route that deals with events and upon receiving a new call, check if there is not a similar call that was made previously.
Assuming this function:
const createCustomerBankAccount = function(customer, params){
// Model validation
...
// Creating the account in the database
db.customerBankAccount.create({ ...params, customer: customer.id })
}
Applying this pattern, it can be rewritten as follows:
const createCustomerBankAccount = function(customer, params){
const newRequest = { customer, ...params }
const existingRequest = db.events.get(newRequest);
if (existingRequest){
return;
}
// Model validation
...
// Creating the account in the database
db.customerBankAccount.create({ ...params, customer: customer.id })
// Saving the event for future calls
db.events.create({...newRequest})
}
Or, we can use hashing to save storage:
const createCustomerBankAccount = function(customer, params){
const newRequestHash = MD5(JSON.stringify({ customer, ...params }));
const existingRequest = db.events.get(hash: newRequestHash);
if (existingRequest){
return;
}
// Model validation
...
// Creating the account in the database
db.customerBankAccount.create({ ...params, customer: customer.id })
db.events.create({ hash: newRequestHash})
}
Idempotence-Key Pattern
The idea behind this pattern is to use a key to represent an operation, and upon receiving a duplicate key, not repeat the operation.
We use a similar pattern to this in our APIs through the correlationID property.
Assuming this function:
const createCustomerBankAccount = function(customer, params){
// Model validation
...
// Creating the account in the database
db.customerBankAccount.create({ ...params, customer: customer.id })
}
Applying this pattern, it can be rewritten as follows:
const createCustomerBankAccount = function(customer, params){
const { idempotenceKey } = params;
// Check if the user sent an idempotence key
if (idempotenceKey){
const existingBankAccount = db.customerBankAccount.get({
idempotenceKey
})
// Checking if there is any registered account with this key and returning it if we find
if (existingBankAccount){
return existingBankAccount;
}
}
// Model validation
...
// Creating the account in the database
db.customerBankAccount.create({ ...params, customer: customer.id });
}
If you want to work in a startup in its early stages, This is your chance. Apply today!
Woovi is a Startup that enables shoppers to pay as they please. To make this possible, Woovi provides instant payment solutions for merchants to accept orders.
If you want to work with us, we are hiring!
Top comments (0)