1. Solicitud repetida
Ya hemos experimentado el riesgo de duplicados después de la creación con POST. Ahora es importante observar todos los métodos desde este mismo punto de vista: ¿qué sucederá si una misma solicitud se envía repetidamente?
Imagina que, como cliente, enviaste una solicitud y luego... la enviaste de nuevo. A veces esto no ocurre porque seas malintencionado, sino porque la realidad es impredecible. El usuario puede hacer doble clic en el botón "Agregar", el navegador puede "saltar" al actualizar la página, o alguna capa intermedia puede repetir la solicitud porque "no está segura de que haya llegado". Incluso si no analizas la red en profundidad, el hecho mismo de las repeticiones es algo cotidiano, no una rareza exótica.
Y aquí surge una pregunta muy práctica para el desarrollador backend: repetir una solicitud, ¿es seguro o peligroso? Si la repetición convierte "un libro en la lista" en "dos libros idénticos en la lista", el usuario mirará tu servicio con el mismo afecto que a un despertador que decide sonar dos veces seguidas a las 6:00.
Para discutir esto con tranquilidad, necesitamos un concepto que explique el efecto de repetir una solicitud.
2. Idempotencia sin academicismo
La palabra "idempotencia" parece diseñada para ahuyentar a los principiantes. En la práctica, significa algo muy simple: si repetimos la misma solicitud varias veces, el estado final de los datos será el mismo que después de la primera aplicación exitosa. No "la respuesta será idéntica hasta el último byte", sino que el efecto sobre los datos será el mismo estado final.
Es importante separar dos ideas que los principiantes suelen confundir. Primera: la idempotencia no garantiza que el servidor deba devolver exactamente el mismo texto/JSON al repetir. Segunda: la idempotencia no significa "la operación no cambia nada". Puede modificar datos, simplemente la repetición no debe cambiarlos más allá.
Una analogía doméstica muy útil es la simple asignación en Java. Si asignas el mismo valor a una variable dos veces seguidas, el resultado no cambia después de la primera vez. Esto ya es casi idempotencia, solo que sin HTTP.
// Estado inicial de la variable
String status = "PLANNED";
// Primer cambio de estado
status = "FINISHED";
// Repetimos el mismo cambio: el resultado no cambia
status = "FINISHED";
System.out.println(status); // FINISHED
Aquí, repetir "hacer FINISHED" no produjo nada nuevo. El estado final después de la segunda vez es el mismo que después de la primera.
Ahora comparemos con agregar a una lista, donde repetir "agregar" produce un nuevo efecto:
import java.util.ArrayList;
import java.util.List;
List<String> titles = new ArrayList<>();
// Primera vez que añadimos un elemento
titles.add("Clean Code");
// Repetimos: obtuvimos un segundo elemento idéntico
titles.add("Clean Code");
System.out.println(titles.size()); // 2
La repetición de "crear/agregar" dio dos elementos. Esto ya se parece al comportamiento típico de POST.
3. Repeticiones de lectura: GET como método tranquilo
Con la lectura, todo es agradable y predecible: si simplemente obtienes datos de un recurso, repetir la solicitud no debe cambiar el estado del recurso. Esta es la intuición básica que reforzamos en la lección sobre GET. Por lo tanto, GET en el sentido típico de aplicación no solo es idempotente, sino también "seguro" (safe): no modifica los datos en absoluto.
En el dominio ReadLater, esto se ve así: lees la lista de libros o un libro específico. Repites y obtienes lo mismo. Nada grave. Incluso si lees diez veces seguidas, la lista no debería "multiplicarse".
Como un mini-modelo (sin HTTP real), se puede representar que GET es simplemente "tomar el valor del almacenamiento":
import java.util.Map;
// Almacén condicional (por ejemplo, base de datos o caché)
Map<Long, String> storage = Map.of(
42L, "Effective Java",
7L, "Clean Code"
);
// Dos lecturas de la misma clave
String firstRead = storage.get(42L);
String secondRead = storage.get(42L);
System.out.println(firstRead); // Effective Java
System.out.println(firstRead.equals(secondRead)); // true
Sí, en la vida real la respuesta puede diferir (por ejemplo, los datos podrían haber sido actualizados por otra solicitud entre las lecturas), pero la operación "leer" no está obligada a modificar los datos.
En términos de HTTP, esto suena igual de simple:
// Simplemente formamos la cadena de solicitud (pseudo-HTTP para ilustración)
String request = "GET /reading-list/42";
System.out.println(request); // GET /reading-list/42
La repetición de "GET del mismo recurso" no conlleva ningún peligro, por lo que GET es tu mejor amigo cuando se trata de solicitudes repetidas.
4. Reemplazo completo: PUT como asignación
Ahora tomemos una situación más compleja: no estamos leyendo, sino modificando el recurso. Suena más grave, pero aquí PUT se comporta de manera sorprendentemente amigable si su significado es realmente "reemplazo completo". La lógica es: el cliente dice "aquí está la versión completa de cómo debe ser el recurso", y el servidor lleva el recurso a ese estado. Repetir el mismo PUT significa volver a llevar el recurso al mismo estado. Después de la primera vez ya estás allí, la repetición no cambia el resultado.
Si lo traducimos a una analogía en Java, es nuevamente como una asignación. No "agregamos otro libro", sino "hacemos que el recurso sea así".
Imaginemos que tenemos un almacenamiento y "reemplazamos" el valor por clave:
import java.util.HashMap;
import java.util.Map;
Map<Long, String> titlesById = new HashMap<>();
// Inicialmente el recurso tenía un título antiguo
titlesById.put(42L, "Some Old Title");
// Reemplazo completo del valor por clave (análogo a PUT replace)
titlesById.put(42L, "Effective Java");
// La repetición del mismo reemplazo: el estado sigue siendo el mismo
titlesById.put(42L, "Effective Java");
System.out.println(titlesById.get(42L)); // Effective Java
Después del primer put(42L, "Effective Java"), el recurso ya está "en el estado deseado". La repetición no crea un segundo objeto con id=42 (eso no ocurre), no incrementa un contador, no agrega una copia; simplemente vuelve a fijar el mismo estado.
En "términos HTTP", esto suele verse así:
// Pseudosolicitud PUT: versión completa del recurso en el cuerpo
String request = """
PUT /reading-list/42
{ "title": "Effective Java", "author": "Joshua Bloch" }
""";
System.out.println(request);
El significado de PUT ayuda mucho a la disciplina de la API: si quieres que la repetición de una solicitud sea predecible, PUT es un candidato natural para el "reemplazo completo". Sí, modifica datos, pero lo hace de manera que la repetición no debe cambiar el resultado final.
5. Eliminación: DELETE y el estado final
La eliminación a menudo se percibe como "oh, miedo, estamos rompiendo todo". Pero en realidad, la eliminación tiene una idea muy conveniente de estado final: después de una eliminación exitosa, el recurso ya no existe. Y si repites la eliminación del mismo recurso, intentas llegar nuevamente al mismo estado final: "el recurso no existe". Lógicamente, ya has alcanzado el objetivo.
En una analogía en Java, esto se parece a establecer el indicador exists = false o a remove de un Map.
import java.util.HashMap;
import java.util.Map;
Map<Long, String> items = new HashMap<>();
items.put(42L, "Effective Java");
// Primera eliminación: la clave desaparece
items.remove(42L);
// La repetición de la eliminación: la clave sigue sin existir, el estado no cambia
items.remove(42L);
System.out.println(items.containsKey(42L)); // false
El estado final después de la primera eliminación es "la clave no existe". Después de la segunda, sigue siendo "la clave no existe".
En forma HTTP:
// Pseudo-HTTP: eliminamos un recurso específico
String request = "DELETE /reading-list/42";
System.out.println(request); // DELETE /reading-list/42
Aquí hay un matiz importante que no convertiremos en una discusión separada sobre estándares: diferentes sistemas pueden responder de manera diferente a un DELETE repetido (por ejemplo, "no encontrado" o "ya eliminado"). Pero la idempotencia se trata del estado final, no de un texto de respuesta idéntico. El estado "sin recurso" ya se ha alcanzado.
6. Creación: POST y duplicados
Con POST todo es más divertido, y es precisamente por eso que POST suele ser una fuente de sorpresas. Cuando haces un POST en un escenario CRUD típico, estás diciendo: "crea un nuevo elemento en la colección". Un nuevo elemento generalmente recibe un nuevo identificador asignado por el servidor. Por lo tanto, repetir el mismo POST normalmente crea otro elemento. Incluso si los campos son idénticos, el resultado son dos recursos diferentes (por ejemplo, id=1 e id=2).
Una analogía cotidiana es add en una lista:
import java.util.ArrayList;
import java.util.List;
List<String> readingList = new ArrayList<>();
// Primera creación (adición a la colección)
readingList.add("Clean Code");
// La repetición de la misma "creación" genera un duplicado
readingList.add("Clean Code");
System.out.println(readingList); // [Clean Code, Clean Code]
Si queremos vincularlo a nuestra futura lógica de "el servidor asigna el id", podemos mostrar una analogía con un "generador de id":
// El generador de id más simple (para ilustración)
long nextId = 1L;
// Cada "crear" produce un nuevo identificador
long firstId = nextId++;
long secondId = nextId++;
System.out.println(firstId); // 1
System.out.println(secondId); // 2
Cada llamada a "crear" produce un nuevo resultado. Incluso si los datos de entrada son los mismos, el servidor tiene derecho a crear un nuevo recurso. Por lo tanto, en el sentido típico, POST es no idempotente.
En una pseudoforma HTTP, se ve así:
// Pseudosolicitud POST: creamos un elemento en la colección
String request = """
POST /reading-list
{ "title": "Clean Code", "author": "Robert C. Martin" }
""";
System.out.println(request);
Repetirlo dos veces normalmente significa dos elementos en la colección. Y esto no es un "error HTTP", es el significado normal de "crear uno nuevo".
Si te parece injusto para el usuario ("pero él solo hizo doble clic"), ya has identificado la razón por la que los desarrolladores backend valoran tanto la idempotencia y aman los métodos "predecibles" para escenarios predecibles. Pero las formas de combatir estos duplicados ya son una tarea aplicada aparte. Por ahora, es importante recordar honestamente lo básico: un POST repetido a menudo significa una creación repetida.
7. PATCH: set frente a delta
Con PATCH hay un matiz por el cual los principiantes o lo temen o lo usan como un "actualizador mágico de todo". PATCH trata sobre el cambio parcial, pero el cambio parcial puede ser de dos tipos: "establecer un valor" y "aplicar un delta". En el primer caso, la repetición suele ser idempotente (como una asignación). En el segundo, la repetición puede cambiar el estado cada vez (como "sumar 1").
Lo mostraré con analogías muy simples en Java.
Si PATCH significa "establecer el estado en FINISHED", la repetición no es peligrosa:
// Estado original
String status = "PLANNED";
// PATCH como "set": establecer un valor específico
status = "FINISHED";
// La repetición del mismo set: el estado no cambia
status = "FINISHED";
System.out.println(status); // FINISHED
Y si PATCH significa "alternar" o "incrementar", la repetición ya cambia el resultado:
int likes = 0;
// PATCH condicional como "delta": incrementar el valor
likes++;
// Repetición: el valor aumentará una vez más
likes++;
System.out.println(likes); // 2
O "toggle":
boolean enabled = false;
// toggle: cada aplicación cambia el estado al opuesto
enabled = !enabled;
// la repetición de toggle lo devuelve
enabled = !enabled;
System.out.println(enabled); // false
Es decir, PATCH por sí mismo no garantiza la idempotencia. Todo depende del significado de la operación.
En nuestro futuro dominio ReadLater, un escenario PATCH muy lógico es "cambiar el estado del libro" a un valor específico. Si hacemos una solicitud del tipo "poner el estado en FINISHED", se puede repetir tranquilamente: después de la primera vez, el libro ya está FINISHED. Y este es un buen diseño, porque PATCH sigue siendo específico y claro: solo cambiamos el estado, no tocamos el resto.
8. Resumen para ReadLater
Cuando ya tienes claro el significado de los métodos, es útil tener una imagen compacta: qué métodos suelen ser idempotentes y cuáles no. Desde ya aclaro: esto no es una "ley de la naturaleza", sino una semántica CRUD aplicada típica, que es conveniente seguir en escenarios backend habituales.
| Método | Significado típico en el modelo de recursos | ¿Modifica datos? | ¿Es idempotente al repetirse? | Ejemplo para ReadLater |
|---|---|---|---|---|
| GET | Leer recurso | No | Sí | GET /reading-list/42 |
| POST | Crear un nuevo elemento en la colección | Sí | Generalmente no | POST /reading-list (crear un libro) |
| PUT | Reemplazar completamente el recurso | Sí | Sí (si es "replace") | PUT /reading-list/42 |
| PATCH | Modificar parcialmente el recurso | Sí | Depende del significado | PATCH /reading-list/42 (por ejemplo, estado) |
| DELETE | Eliminar recurso | Sí | Sí (estado final "sin recurso") | DELETE /reading-list/42 |
Este resumen responde solo a una pregunta: ¿qué hace la repetición de la misma solicitud con el estado del recurso? Los códigos de estado, los encabezados y la elección entre path/query/body son otro eje del contrato HTTP.
Ahora visualmente (a veces es más fácil "ver" las repeticiones como transiciones de estados). Aquí hay un diagrama para un solo recurso /reading-list/42:
stateDiagram-v2
[*] --> Exists : recurso existe
Exists --> Exists : "PUT (reemplazo idempotente)"
Exists --> Exists : "PATCH (actualización idempotente)"
Exists --> Exists : "GET (lectura, sin cambio de estado)"
Exists --> Deleted : "DELETE (recurso eliminado)"
Deleted --> Deleted : "DELETE (repetición — sin recurso)"
Deleted --> Deleted : "GET (la lectura no cambia el estado)"
Y para la colección /reading-list, el matiz clave es diferente: POST generalmente agrega un nuevo elemento, y la repetición de la solicitud agregará otro. Por lo tanto, las colecciones son el lugar donde los duplicados aparecen con más frecuencia si no se diseña la semántica.
9. Errores típicos al trabajar con idempotencia
Error n.º 1: pensar que la idempotencia significa "el servidor debe devolver la misma respuesta".
Los principiantes a menudo verifican la idempotencia visualmente: "si la segunda respuesta es diferente, entonces no es idempotente". Esta es una verificación incorrecta. La idempotencia trata sobre el estado final de los datos. La respuesta puede incluir la hora, un identificador aleatorio, el orden de los elementos o campos adicionales, y eso no anula la idempotencia de la operación como efecto.
Error n.º 2: considerar que idempotencia es igual a "no cambia nada".
Esto ocurre porque GET realmente no cambia nada y además es idempotente. Pero PUT y DELETE modifican datos y, aun así, en el modelo típico siguen siendo idempotentes: un "replace" repetido no cambia el resultado final, un "eliminar" repetido lleva al mismo estado "sin recurso".
Error n.º 3: confundir POST y PUT porque ambos tienen cuerpo.
Es un fallo lógico muy común: "si hay un cuerpo JSON, entonces es POST". En HTTP, el cuerpo es solo una forma de transmitir datos, no el significado. El significado lo da el método. POST generalmente crea un nuevo elemento en la colección y al repetirlo puede crear otro. PUT generalmente reemplaza un recurso existente y al repetirlo lleva al mismo resultado final.
Error n.º 4: usar PATCH como "actualizar algo" sin una semántica clara.
Si PATCH no está limitado a una parte comprensible del recurso y a un efecto comprensible, pierdes dos cosas a la vez: la legibilidad de la API y la posibilidad de razonar sobre la idempotencia. "Un PATCH que hace algo" es casi siempre una invitación a sorpresas, especialmente con repeticiones. Es mejor cuando PATCH se ve como "set status", no como "ejecuta magia".
Error n.º 5: ignorar las causas cotidianas de repeticiones.
A veces, el desarrollador diseña la API como si las solicitudes nunca se repitieran: "bueno, el cliente llamará solo una vez". En la realidad, los usuarios hacen doble clic, las interfaces reenvían, la gente actualiza la página. Si de antemano te planteas la pregunta "¿y si se repite?", automáticamente haces la API más predecible, incluso antes de empezar a escribir el código del servidor.
GO TO FULL VERSION