Monday 29 April 2024

Understanding Idempotent HTTP Requests: Building Reliable APIs

In the world of web development and API design, understanding and designing idempotent HTTP requests is crucial. In this blog post, we'll delve into what idempotence means in the context of HTTP requests, why it's important, and how you can design and implement idempotent APIs effectively.

 

What is idempotence?

Idempotence refers to the property of certain operations where applying them multiple times has the same effect as applying them once. In the context of HTTP requests, this means that making the same request multiple times should not change the system state beyond what would occur with a single request. This property is crucial for building resilient and fault-tolerant systems, especially in distributed environments where network failures and retries are common.

 

Consider when you initiate a web request by clicking a button, prompting the server to execute a task. In the case of idempotent requests, even if you submit the identical request multiple times (perhaps due to connectivity issues or accidental double-clicks), the server will execute the action just once. This safeguard is crucial for mitigating errors, particularly in instances where factors like network instability might lead to unintended request repetitions.

 

Idempotent HTTP Methods

Following table summarizes the core http methods that exhibit idempotent behaviour.

 

Method

Description

GET

Since GET requests are used for retrieving data and do not modify the server state, they are inherently idempotent.

PUT

Used for updating a resource at a specific URI, PUT requests are inherently idempotent. Repeated PUT requests with the same payload to the same URI will result in the same resource state.

DELETE

Deleting a resource using the DELETE method is idempotent. If the resource is already deleted, subsequent DELETE requests will have no effect.

 

Why POST method is not idempotent?

The POST method in HTTP is not inherently idempotent because it is typically used to create new resources on the server. Each time a POST request is made, it usually results in the creation of a new resource or the execution of a specific action.

 

While POST requests are not inherently idempotent, we can enforce idempotence within the context of a specific API design. Developers can implement strategies such as checking for existing resources before creating new ones, using unique identifiers, or designing the server-side processing logic to handle duplicate requests gracefully. However, by default, the POST method itself does not guarantee idempotence.

 

Why PATCH method is not idempotent?

The PATCH method in HTTP is not inherently idempotent because its behaviour depends on the specific modifications it applies to a resource.

 

For example, if a PATCH request adds an item to a list in a resource, applying the same PATCH request multiple times would result in the item being added multiple times, potentially changing the state of the resource with each request.

 

How to address duplicate API requests for non-idempotent APIs?

Using "Idempotency-Key" header, we can address this duplicate APIs for non-idempotent APIs.

 

The "Idempotency-Key" header allows clients to indicate that a particular request should be treated as idempotent. By including a unique identifier in the header, clients can ensure that duplicate requests with the same identifier have no additional effect beyond the first request. This specification is particularly useful for scenarios where idempotent behaviour is desired for operations that would traditionally be performed using non-idempotent HTTP methods. It provides a standardized way for clients to communicate their intent for idempotent requests to servers, enhancing interoperability and reliability in distributed systems.

 

How Idempotency-Key header works from client to server?

Here's a step-by-step procedure for how the Idempotency-Key header works from the client to the server:

 

1.   Client end:

The client prepares an HTTP request to be sent to the server. This request can use any HTTP method, including non-idempotent methods like POST, PUT, or PATCH. Before sending the request, the client includes the Idempotency-Key header in the HTTP request headers. This header contains a unique identifier chosen by the client to represent the idempotent operation. The value of the Idempotency-Key header should be a string that uniquely identifies the operation. This could be a UUID, a timestamp, a randomly generated token, or any other unique identifier chosen by the client.

 

The client sends the HTTP request, including the Idempotency-Key header, to the server.

2.   Server end

Upon receiving the request, the server examines the Idempotency-Key header.

If the server has not previously processed a request with the same Idempotency-Key value, it proceeds to process the request as usual. If the server has already processed a request with the same Idempotency-Key value, it recognizes the request as a duplicate. Instead of executing the operation again, the server retrieves the result or outcome of the previous operation associated with the same Idempotency-Key value and  generates an appropriate HTTP response.

 

Example

public Employee createEmployee(EmployeeRequestPayload empToCreate) {

	String idempotencyKey = httpServletRequest.getHeader(IDEMPOTENCY_KEY);
	if (idempotencyKey != null) {
		synchronized (EMPLOYEE_MAP) {
			Employee emp = EMPLOYEE_MAP.get(idempotencyKey);
			if (emp != null) {
				return emp;
			}

			emp = Employee.newEmployee(empToCreate);
			EMPLOYEE_MAP.put(idempotencyKey, emp);
			return emp;
		}
	}

	throw new IllegalArgumentException(IDEMPOTENCY_KEY + " header is missing");
}

 

The createEmployee method processes requests to create employees. It begins by retrieving the value of the Idempotency-Key header from the HTTP request. If the header is present, the method checks if an employee with the same idempotency key already exists. If found, it returns the existing employee. If not, a new employee is created, associated with the idempotency key, and stored in a synchronized map. If the Idempotency-Key header is missing, the method throws an IllegalArgumentException.

 

 

Find the below working application that use ‘Idempotency-Key’ header to handle idempotent operations.

 

Step 1: Create new maven project ‘idempotency-key-demo’.

 

Step 2: Update pom.xml with maven dependencies.

 

pom.xml

 

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.sample.app</groupId>
	<artifactId>idempotency-key-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<properties>
		<maven.compiler.source>17</maven.compiler.source>
		<maven.compiler.target>17</maven.compiler.target>
	</properties>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.2.2</version>
	</parent>

	<dependencies>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springdoc</groupId>
			<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
			<version>2.0.4</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

 

Step 3: Define model classes.

 

EmployeeRequestPayload.java

 

package com.sample.app.model;

public class EmployeeRequestPayload {
	private String firstName;
	private String lastName;

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

}

 

Employee.java

package com.sample.app.model;

import java.util.concurrent.atomic.AtomicInteger;

public class Employee {
	private static final AtomicInteger EMPLOYEE_ID_GENERATOR = new AtomicInteger(0);

	private Integer id;
	private String firstName;
	private String lastName;

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public static Employee newEmployee(EmployeeRequestPayload empRequestPayload) {
		Employee emp = new Employee();

		emp.setId(EMPLOYEE_ID_GENERATOR.incrementAndGet());
		emp.setFirstName(empRequestPayload.getFirstName());
		emp.setLastName(empRequestPayload.getLastName());

		return emp;
	}
}

 

EmployeeResponse.java

package com.sample.app.model;

import java.util.List;

public class EmployeesResponse {

	private List<Employee> emps;

	public List<Employee> getEmps() {
		return emps;
	}

	public void setEmps(List<Employee> emps) {
		this.emps = emps;
	}

}

 

Step 2: Define service classes.

 

EmployeeService.java

 

package com.sample.app.service;

import com.sample.app.model.Employee;
import com.sample.app.model.EmployeeRequestPayload;
import com.sample.app.model.EmployeesResponse;

public interface EmployeeService {
	
	Employee createEmployee(EmployeeRequestPayload empToCreate);
	
	EmployeesResponse emps();

}

 

EmployeeServiceImpl.java

package com.sample.app.service.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.sample.app.model.Employee;
import com.sample.app.model.EmployeeRequestPayload;
import com.sample.app.model.EmployeesResponse;
import com.sample.app.service.EmployeeService;

import jakarta.servlet.http.HttpServletRequest;

@Service
public class EmployeeServiceImpl implements EmployeeService {
	private static final String IDEMPOTENCY_KEY = "Idempotency-Key";
	private static final Map<String, Employee> EMPLOYEE_MAP = new ConcurrentHashMap<>();

	@Autowired
	private HttpServletRequest httpServletRequest;

	@Override
	public Employee createEmployee(EmployeeRequestPayload empToCreate) {

		String idempotencyKey = httpServletRequest.getHeader(IDEMPOTENCY_KEY);
		if (idempotencyKey != null) {
			synchronized (EMPLOYEE_MAP) {
				Employee emp = EMPLOYEE_MAP.get(idempotencyKey);
				if (emp != null) {
					return emp;
				}

				emp = Employee.newEmployee(empToCreate);
				EMPLOYEE_MAP.put(idempotencyKey, emp);
				return emp;
			}
		}

		throw new IllegalArgumentException(IDEMPOTENCY_KEY + " header is missing");
	}

	@Override
	public EmployeesResponse emps() {
		List<Employee> emps = new ArrayList<>(EMPLOYEE_MAP.values());
		EmployeesResponse empResponse = new EmployeesResponse();
		empResponse.setEmps(emps);
		return empResponse;
	}

}

Step 3: Define EmployeeController class.

 

EmployeeController.java

package com.sample.app.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.sample.app.model.Employee;
import com.sample.app.model.EmployeeRequestPayload;
import com.sample.app.model.EmployeesResponse;
import com.sample.app.service.EmployeeService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;

@RestController
@RequestMapping("/api/v1/employees")
@Tag(name = "employees", description = "This section contains APIs related to employees")
@CrossOrigin("*")
public class EmployeeController {

	@Autowired
	private EmployeeService empService;

	@Parameter(name = "Idempotency-Key", in = ParameterIn.HEADER, description = "Idempotency-Key header", required = true)
	@PostMapping
	@Operation(summary = "Save the employee")
	public ResponseEntity<Employee> save(@RequestBody EmployeeRequestPayload dto) {
		return ResponseEntity.status(HttpStatus.CREATED).body(empService.createEmployee(dto));
	}

	@GetMapping
	@Operation(summary = "Get all employees")
	public ResponseEntity<EmployeesResponse> employees() {
		EmployeesResponse empResponse = empService.emps();
		return ResponseEntity.ok(empResponse);
	}

}

Step 4: define swagger configuration class.

 

SwaggerConfig.java

package com.sample.app.config;

import org.springframework.context.annotation.Configuration;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;

@Configuration
@OpenAPIDefinition(info = @Info(title = "Idempotency key demo", version = "v1"))
public class SwaggerConfig {

}

Step 5: Define main application class.

 

App.java

package com.sample.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

	public static void main(String[] args) {
		SpringApplication.run(App.class, args);
	}

}

Total project structure looks like below.



Build the artifact

Go to the project, where pom.xml file is located and run the below command.

 

mvn package

 

Execute below command to run the application.

java -jar ./target/idempotency-key-demo-0.0.1-SNAPSHOT.jar

 

Open below url ‘http://localhost:8080/swagger-ui/index.html’ to experiment with swagger documentation.


You can download this application from this link.

 

References

https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-04

 

No comments:

Post a Comment