Read in English · Lire en français · Auf Deutsch lesen
Sección 6.5 de 6.8 · Intermedio · 30 min de lectura
Comunicación síncrona y asíncrona: REST, gRPC, GraphQL y mensajería
Videolección
5 min · narrado en inglés, con subtítulosSolo escuchar (audio)
Leer la transcripción
Una vez que un sistema se divide, sus partes deben comunicarse a través de una red, y cómo se comunican da forma al sistema tanto como cómo se divide. En esta lección aprenderás los dos modos básicos, las principales tecnologías y cómo diseñar API que perduren.
Los servicios se comunican de dos maneras. Una llamada telefónica es síncrona: esperas en la línea hasta obtener una respuesta y, si no hay nadie, no obtienes nada. Una carta es asíncrona: la envías y sigues con tu día, y la respuesta llega más tarde. Las llamadas telefónicas son sencillas, pero atan a ambas personas al mismo momento. Las cartas son más flexibles, pero no sabes enseguida si tu solicitud ha funcionado. En ambos casos, las dos partes necesitan un acuerdo compartido y por escrito sobre lo que se puede pedir, y una forma de cambiarlo sin perjudicar a quienes dependen de él.
En la fila superior, la app del cliente llama al servicio de pedidos, que llama al servicio de pagos, y cada salto espera. Mientras espera, el servicio de pedidos está bloqueado. Los tres deben estar activos en el mismo momento, así que las latencias se suman y las disponibilidades se multiplican. Eso es el acoplamiento temporal. En la fila inferior, el servicio de pedidos envía un mensaje de pedido realizado a una cola y sigue adelante. El mensaje espera hasta que el servicio de notificaciones está listo, así que el receptor puede estar ocupado o caído brevemente sin que el emisor lo note. Pero la asincronía traslada la complejidad en lugar de eliminarla. Ambas partes siguen dependiendo del formato y del significado del mensaje. Una buena regla: llama de forma síncrona solo cuando quien llama realmente no pueda continuar sin la respuesta.
REST, abreviatura de Representational State Transfer, usa recursos, métodos y códigos de estado tal como los define HTTP. Funciona bien con la caché de las herramientas estándar y es adecuado para API públicas y de socios. gRPC envía mensajes binarios compactos, con código tipado generado a partir de un archivo de contrato. Es popular para las llamadas internas entre servicios, pero los navegadores no pueden llamarlo directamente y sus mensajes son difíciles de leer en los logs. GraphQL permite que cada cliente pida exactamente los campos que necesita, en un solo viaje de ida y vuelta. Pero el equipo del servidor debe limitar las consultas costosas y comprobar los permisos campo a campo. Y la mensajería, mediante colas y topics, es adecuada para el trabajo que puede hacerse más tarde, el fan-out a muchos consumidores y el suavizado de la carga.
Ahora, los reintentos. En HTTP, GET solo lee, así que es seguro repetirlo. PUT y DELETE son idempotentes. Pero POST no es ninguna de las dos cosas, así que reintentarlo puede crear un segundo pedido y cobrar dos veces al cliente. La solución es una clave de idempotencia. La app envía su pedido con una clave única. La API guarda juntos el pedido y la clave, y la base de datos confirma el pedido mil cuarenta y dos. Pero la respuesta se pierde por el camino de vuelta, así que la app agota el tiempo de espera y reintenta. El reintento lleva la misma clave. La API ve que ya está hecho y devuelve la respuesta guardada para el pedido mil cuarenta y dos. Ni segundo pedido ni segundo cobro.
El ejemplo práctico de la sección añade tres reglas. Si el cliente vuelve a tocar mientras la primera solicitud aún se está ejecutando, el servicio responde con un conflicto, y la app espera y reintenta. Nunca se ejecutan dos copias a la vez. Si un error de programación envía la misma clave con un carrito distinto, el servicio la rechaza en lugar de devolver en silencio el pedido equivocado. Y las claves caducan tras un periodo publicado, aquí veinticuatro horas. La clave se crea una vez por operación lógica, no por intento, y se guarda en la misma transacción que el pedido, así que una caída no puede separarlos.
Una API con consumidores es una promesa. Los cambios compatibles hacia atrás, como un campo opcional o un nuevo endpoint, pueden hacerse libremente. Eliminar o renombrar un campo rompe a los consumidores. Cambiar el significado de un campo es la ruptura más peligrosa, porque nada falla de forma visible. Los consumidores ayudan siendo lectores tolerantes: ignoran los campos que no entienden, así que los añadidos nunca los rompen. Cuando una ruptura es inevitable, versiona la API. Entre tus propios servicios, puedes expandir y contraer en su lugar: añadir el nuevo campo, pasar a todos los consumidores y después eliminar el antiguo. Y las pruebas de contrato dirigidas por el consumidor permiten que cada consumidor declare lo que realmente usa, así que la compilación del proveedor muestra qué cambios romperían a quién. Eso es lo que hace seguro el despliegue independiente.
La sección completa trata REST tal como se definió originalmente, la paginación, los formatos de error y las herramientas de contratos. A continuación, en la Sección seis punto seis, veremos los modelos de consistencia, CAP y PACELC. Nos vemos allí.
Lo que necesitas saber
- Las llamadas síncronas crean acoplamiento temporal: quien llama y quien recibe la llamada deben estar activos a la vez, de modo que a lo largo de una cadena las latencias se suman y las disponibilidades se multiplican; la mensajería asíncrona elimina ese acoplamiento, pero no el acoplamiento de formato ni el semántico.
- REST es el conjunto de restricciones de Fielding (cliente-servidor, sin estado, almacenable en caché, interfaz uniforme, sistema en capas y, opcionalmente, código bajo demanda); la mayoría de las API reales alcanzan el nivel 2 del modelo de madurez de Richardson, no el nivel 3 de hipermedia.
- El RFC 9110 define GET, HEAD, OPTIONS, PUT y DELETE como idempotentes, y POST como ni seguro ni idempotente; los clientes solo deberían reintentar automáticamente las solicitudes idempotentes tras un timeout, un 502, un 503 o un 504, por lo que POST necesita una clave de idempotencia para poder reintentarse con seguridad.
- gRPC usa Protocol Buffers sobre HTTP/2, con código generado y streaming, y es adecuado para llamadas internas; GraphQL permite que clientes muy distintos elijan los campos, pero necesita agrupación de consultas (batching), límites al costo de las consultas y autorización a nivel de campo.
- Las colas entregan cada mensaje a un solo consumidor de entre varios que compiten; los topics entregan una copia a cada suscriptor.
- Las API duraderas modelan recursos del dominio, paginan con cursores, devuelven problem details del RFC 9457, cambian solo de forma compatible hacia atrás y se versionan o usan expandir y contraer cuando tienen que romper la compatibilidad.
- OpenAPI y AsyncAPI describen contratos HTTP y de mensajes; las pruebas de contrato dirigidas por el consumidor, como Pact, permiten a los proveedores saber qué cambios romperían a qué consumidores.
Dos formas de comunicarse y el acoplamiento que crea cada una
Una vez que un sistema se divide en partes que se despliegan por separado (consulta por qué distribuir), esas partes deben comunicarse a través de una red, y la forma en que se comunican da forma al sistema tanto como la forma en que se dividen. Hay dos modos básicos. En la comunicación síncrona, quien llama envía una solicitud y espera la respuesta antes de continuar, como en una llamada telefónica. En la comunicación asíncrona, el emisor entrega un mensaje a un intermediario, normalmente un intermediario de mensajes (message broker), y sigue adelante; el receptor recoge el mensaje cuando está listo, como una carta en un buzón. La Sección 0.6 presentó la diferencia; aquí se examina como una decisión arquitectónica.
Las llamadas síncronas crean acoplamiento temporal: ambas partes deben estar en ejecución y accesibles en el mismo momento. Por tanto, quien llama es, como mucho, tan disponible como el producto de las disponibilidades de sus dependencias, y su tiempo de respuesta, incluida su latencia de cola, contiene el de ellas: es la aritmética de la Sección 6.1. Un servicio de pagos lento hace lento al servicio de pedidos y, si todos los hilos del servicio de pedidos están esperándolo, el servicio de pedidos deja de responder a nadie. Los timeouts, los reintentos y los circuit breakers limitan el daño y se tratan en patrones de resiliencia.
La mensajería asíncrona elimina el acoplamiento temporal: el intermediario guarda el mensaje, así que el receptor puede estar lento, ocupado o caído brevemente sin que el emisor lo note. No elimina todo tipo de acoplamiento. Ambas partes siguen dependiendo del formato y del significado del mensaje (acoplamiento de formato y semántico), y del propio intermediario, que se convierte en infraestructura crítica. El emisor también pierde el conocimiento inmediato del resultado, por lo que el diseño debe hacer frente a trabajo que falla más tarde, a mensajes que llegan dos veces y a mensajes que llegan en otro orden. La asincronía traslada la complejidad en lugar de eliminarla.
Una regla práctica útil: usa una llamada síncrona cuando quien llama realmente no puede continuar sin la respuesta ("¿está autorizada esta tarjeta?"), y mensajería asíncrona cuando quien llama solo necesita saber que el trabajo se hará ("envía el recibo", "avisa al restaurante"). Muchos buenos diseños mantienen corto y síncrono el camino que ve el usuario y ponen todo lo demás detrás de una cola.
Acoplamiento temporal, con y sin intermediario
REST tal como lo definió Fielding
REST (Representational State Transfer) fue definido por Roy Fielding en su tesis doctoral de 2000 como un estilo arquitectónico para grandes sistemas hipermedia en red: la propia web. Fielding lo derivó como un conjunto de restricciones, cada una de las cuales aporta una cualidad. Cliente-servidor separa la interfaz de usuario del almacenamiento de datos para que ambos evolucionen de forma independiente. La interacción sin estado significa que cada solicitud lleva todo lo necesario para entenderla, de modo que cualquier instancia del servidor puede atenderla, lo que favorece la escalabilidad y la recuperación. Las respuestas almacenables en caché indican si pueden reutilizarse, lo que reduce la latencia y la carga. Una interfaz uniforme (recursos identificados por URI, manipulados mediante representaciones, mensajes autodescriptivos e hipermedia como motor del estado de la aplicación) permite que intermediarios genéricos, como cachés y proxies, entiendan el tráfico. Un sistema en capas permite que proxies, gateways y balanceadores de carga se sitúen en el camino de forma invisible. El código bajo demanda, como el JavaScript que se envía a un navegador, es la única restricción opcional.
La mayoría de las API que se describen como RESTful solo cumplen algunas de ellas. El modelo de madurez de Leonard Richardson, presentado en 2008 y popularizado por Martin Fowler en 2010, describe los pasos. En el nivel 0, HTTP es solo un túnel: cada llamada es un POST a un único endpoint con la operación indicada en el cuerpo, como en SOAP. En el nivel 1, la API tiene recursos separados (/orders/1042, /restaurants/7). En el nivel 2, usa los métodos y los códigos de estado HTTP tal como los define HTTP. En el nivel 3, las respuestas llevan controles hipermedia: enlaces que le dicen al cliente qué puede hacer a continuación (un pedido realizado contiene un enlace para cancelarlo, hasta que la cancelación ya no está permitida).
Los profesionales discrepan de verdad sobre el nivel 3. Fielding ha insistido en que una API sin hipermedia no es REST. La mayoría de las API públicas se quedan en el nivel 2 porque sus clientes se escriben a partir de la documentación en lugar de descubrirse en tiempo de ejecución, y el beneficio de seguir enlaces rara vez compensa el diseño adicional. Una lectura pragmática es que el nivel 2, aplicado de forma coherente, aporta la mayor parte del valor; la hipermedia se justifica cuando el servidor debe guiar a muchos clientes escritos de forma independiente a través de un flujo de trabajo que cambia.
La semántica HTTP que importa para la arquitectura
La semántica de HTTP está definida en el RFC 9110 (2022). Dos propiedades de los métodos importan más que cualquier otra para el diseño distribuido. Un método seguro solo lee: GET, HEAD y OPTIONS no cambian nada que el cliente haya pedido cambiar. Un método idempotente puede cambiar el estado, pero repetirlo tiene el mismo efecto previsto que enviarlo una vez: PUT (sustituir este recurso por esta representación) y DELETE son idempotentes, igual que todos los métodos seguros. POST no es ninguna de las dos cosas, y PATCH no es idempotente en general. La propiedad importa porque las redes fallan entre el momento en que se procesa una solicitud y el momento en que llega la respuesta. El RFC 9110 dice que las solicitudes idempotentes pueden repetirse automáticamente tras un fallo así, y que los clientes no deberían reintentar automáticamente solicitudes no idempotentes a menos que sepan que, de hecho, es seguro repetirlas.
Los códigos de estado forman parte del contrato, porque el código del cliente se ramifica según ellos; la Sección 0.6 presentó los más comunes. Los códigos de éxito deberían decir qué ha ocurrido: 201 Created (con un encabezado Location) cuando se crea un recurso, 202 Accepted cuando el trabajo solo se ha puesto en cola, 204 No Content para un éxito sin cuerpo. Para los conflictos, 409 significa que la solicitud choca con el estado actual del recurso, 412 que ha fallado una precondición y 422 que una solicitud bien formada no es válida semánticamente. La regla arquitectónica trata de los reintentos: una respuesta 4xx volverá a fallar si la solicitud se envía sin cambios (429 Too Many Requests es la excepción: espera lo indicado y luego reintenta), mientras que un timeout, un 502, un 503 o un 504 pueden tener éxito más tarde. Los clientes solo deberían reintentar estos, con backoff y jitter, y solo cuando la solicitud es idempotente o lleva una clave de idempotencia.
Otras dos funciones de HTTP resuelven problemas distribuidos a bajo costo. Las solicitudes condicionales proporcionan control de concurrencia optimista: un cliente lee un pedido con su etiqueta de versión (ETag) y luego envía If-Match con esa etiqueta al actualizarlo; si otra persona ha cambiado el pedido entretanto, el servidor responde 412 y no se sobrescribe nada. Los encabezados de caché (Cache-Control, ETag) permiten que navegadores, CDN y proxies reutilicen respuestas, lo que a menudo es la táctica de rendimiento más barata disponible.
Métodos HTTP: seguros, idempotentes o ninguna de las dos cosas (RFC 9110; PATCH según el RFC 5789)
| Método | Uso típico | ¿Seguro? | ¿Idempotente? | ¿Se puede reintentar a ciegas? |
|---|---|---|---|---|
| GET, HEAD | Leer un recurso | Sí | Sí | Sí |
| PUT | Crear o sustituir en un URI conocido | No | Sí | Sí |
| DELETE | Eliminar un recurso | No | Sí | Sí (una segunda llamada puede devolver 404, pero el efecto es el mismo) |
| POST | Crear en una colección o ejecutar una acción | No | No | Solo con una clave de idempotencia u otra comprobación de duplicados |
| PATCH | Actualización parcial | No | No en general | Solo si el parche está escrito para ser idempotente o está condicionado a una versión |
gRPC: primero los contratos, binario en la red
gRPC es un framework de llamadas a procedimientos remotos que Google liberó como código abierto en 2015 como sucesor de Stubby, el sistema RPC que había usado internamente durante más de una década; la versión 1.0 llegó en 2016. El contrato se escribe en un archivo .proto usando Protocol Buffers, un formato de serialización binario compacto, y los generadores de código producen stubs de cliente tipados y esqueletos de servidor en muchos lenguajes. Las llamadas viajan sobre HTTP/2, que multiplexa muchas solicitudes simultáneas en una sola conexión.
gRPC ofrece cuatro tipos de llamada: unaria (una solicitud, una respuesta), streaming del servidor (una solicitud, un flujo de respuestas, como las posiciones de los repartidores en tiempo real), streaming del cliente (un flujo de solicitudes, una respuesta, como al subir telemetría) y streaming bidireccional. Los clientes fijan un plazo (deadline); cuando vence, la llamada termina con DEADLINE_EXCEEDED, y los plazos pueden propagarse a lo largo de una cadena de llamadas para que el trabajo se detenga en todas partes cuando ya nadie espera la respuesta.
Sus puntos fuertes son la eficiencia, el tipado fuerte y el código generado, lo que lo hace popular para las llamadas internas entre servicios en organizaciones políglotas. Sus costos son la imagen especular. Las cargas útiles binarias son difíciles de leer en los logs y con herramientas corrientes. Los navegadores no pueden llamar a gRPC directamente, por lo que las API públicas y las orientadas a la web suelen necesitar un proxy o una fachada REST o GraphQL. La caché HTTP no se aplica de la forma habitual. La compatibilidad depende de la disciplina con los números de campo: es el número de un campo, no su nombre, lo que lo identifica en la red, así que los números nunca deben reutilizarse, y los campos eliminados deberían marcarse como reservados.
syntax = "proto3";
service Dispatch {
rpc AssignCourier (AssignRequest) returns (Assignment);
rpc TrackCourier (TrackRequest) returns (stream Position);
}
message AssignRequest {
string order_id = 1;
string restaurant_id = 2;
reserved 3; // field removed in v2: never reuse 3
}
message Position {
double lat = 1;
double lng = 2;
int64 recorded_at_ms = 3;
}GraphQL: el cliente elige la forma
GraphQL es un lenguaje de consulta y un entorno de ejecución para API que Facebook desarrolló internamente a partir de 2012 para sus apps móviles y publicó en 2015, con un borrador de especificación y una implementación de referencia; en 2018 pasó a la GraphQL Foundation, neutral respecto a los proveedores. El servidor publica un esquema tipado; los clientes envían consultas a un único endpoint indicando exactamente los campos que quieren, posiblemente a través de objetos relacionados, y reciben una respuesta JSON con la misma forma. Las mutaciones cambian datos y las suscripciones envían actualizaciones.
Su principal beneficio es para clientes muy distintos. Una app móvil de reparto de comida con una conexión lenta puede pedir el estado de un pedido, el nombre del restaurante y el nombre de pila del repartidor en un solo viaje de ida y vuelta, sin recibir los otros veinte campos que necesita el panel web. Eso evita tanto el exceso de datos (over-fetching) como la cadena de solicitudes que puede exigir una API REST recurso a recurso, y puede sustituir a un backend separado para cada frontend. El esquema, además, se documenta a sí mismo y permite que las herramientas validen las consultas antes de ejecutarlas.
Los compromisos son reales y los paga sobre todo el equipo del servidor. Cada campo lo rellena un resolver, así que una implementación ingenua que obtiene por separado el restaurante de cada pedido produce el problema de las consultas N+1: una consulta para cincuenta pedidos y luego cincuenta más para sus restaurantes. La solución habitual es la agrupación de consultas (batching) y la caché por solicitud, tal como popularizó la biblioteca DataLoader. Como los clientes pueden componer consultas arbitrariamente profundas, los servidores necesitan límites de profundidad y de costo de las consultas, timeouts y, a menudo, una lista de consultas aprobadas (persistidas) para evitar sobrecargas accidentales o malintencionadas. La caché HTTP es más débil cuando todo es un POST a una única URL. La autorización debe aplicarse por campo o por objeto, no por URL. Y los errores suelen notificarse en un array errors dentro de una respuesta HTTP correcta, algo que pasará por alto la monitorización basada en códigos de estado.
Mensajería: colas y topics
La mensajería traslada la conversación a un intermediario. Como explica arquitectura dirigida por eventos, una cola entrega cada mensaje a uno de varios consumidores que compiten, la forma estándar de escalar un grupo de workers, mientras que un topic entrega una copia a cada suscriptor, de modo que los servicios del restaurante, de despacho de repartidores y de notificaciones pueden reaccionar cada uno a OrderPlaced sin que el servicio de pedidos sepa que existen. Esa sección también trata las garantías de entrega, el orden, los duplicados y las colas de mensajes fallidos; colas y back pressure explica cómo las colas absorben los picos de carga.
La mensajería también puede transportar solicitud y respuesta: el solicitante envía un comando con un ID de correlación y una dirección de respuesta, y empareja la respuesta que llegue con su solicitud. Así se conserva el desacoplamiento temporal de un intermediario y aun así se obtiene una respuesta, a costa de más piezas móviles.
REST, gRPC, GraphQL y mensajería comparados
| Aspecto | REST sobre HTTP | gRPC | GraphQL | Mensajería (colas y topics) |
|---|---|---|---|---|
| Interacción | Solicitud-respuesta síncrona | Síncrona; unaria o streaming | Consultas y mutaciones síncronas; las suscripciones envían datos | Asíncrona; enviar y olvidar (fire-and-forget) o solicitud-respuesta mediante ID de correlación |
| Contrato | Documento OpenAPI (opcional pero habitual) | Archivos .proto (obligatorios) | Esquema (obligatorio) | Documento AsyncAPI y esquemas de mensajes (opcionales pero recomendables) |
| Carga útil | Normalmente texto JSON | Protocol Buffers binario | JSON | Cualquiera: JSON, Avro, Protocol Buffers |
| Acoplamiento temporal | Sí | Sí | Sí | No: el intermediario almacena los mensajes |
| Caché | Excelente: cachés HTTP estándar y CDN | Poco incorporado | Más difícil: necesita consultas persistidas o cachés en el cliente | No procede |
| Soporte en navegadores | Nativo | Necesita un proxy (gRPC-Web) | Nativo | No directamente; mediante un backend o WebSockets |
| Mejor uso | API públicas y de socios; CRUD orientado a recursos | Llamadas internas entre servicios de alto volumen y baja latencia; streaming | Muchos tipos de cliente con necesidades de datos distintas; agregar varios servicios | Trabajo que puede hacerse más tarde; fan-out a muchos consumidores; suavizar la carga |
| Principales trampas | API conversadoras (chatty); convenciones incoherentes | Opaco al depurar; disciplina con los números de campo | Resolvers N+1; costo de consulta sin límite; autorización a nivel de campo | Duplicados, orden, flujos invisibles, operación del intermediario |
Diseñar API que perduren: recursos, paginación y filtrado
Modela los recursos en torno al dominio, no a la base de datos. Expón los sustantivos con los que piensan tus consumidores (/orders, /orders/1042/items, /restaurants/7/menu), no tablas ni servicios internos. Las acciones que no encajan en crear, leer, actualizar y eliminar normalmente pueden modelarse a su vez como recursos: POST /orders/1042/cancellation crea una cancelación, que puede llevar un motivo y ser rechazada con un 409 si la comida ya va de camino. Así se conserva la interfaz uniforme y a la vez se expresa con honestidad el proceso de negocio.
Pagina todas las colecciones, porque las colecciones crecen. La paginación por offset (?offset=40&limit=20) es sencilla y permite saltar a la página 9, pero se vuelve lenta en páginas profundas, porque la base de datos debe saltarse todas las filas anteriores, y muestra duplicados u omite elementos cuando se insertan filas mientras alguien está paginando. La paginación por cursor devuelve un token opaco que apunta justo después del último elemento (?after=eyJpZCI6MTA0Mn0&limit=20); sigue siendo rápida y estable ante las inserciones, a costa de no permitir el acceso aleatorio. Para feeds y para datos grandes o cambiantes, los cursores son la opción por defecto más segura. Limita siempre el tamaño de página en el servidor.
El filtrado y la ordenación deberían usar parámetros de consulta predecibles (?status=delivered&placed_after=2026-10-01&sort=-placed_at) y apoyarse en índices; una API que permite a los clientes filtrar por cualquier campo invita a hacer consultas que la base de datos no puede responder con rapidez. Admite solo los filtros que necesitan los consumidores y documéntalos en el contrato.
Claves de idempotencia: hacer que POST sea seguro de reintentar
El fallo parcial (Sección 6.1) deja a un cliente que no ha recibido respuesta sin saber si su solicitud se procesó. Reintentar un GET es inofensivo; reintentar POST /orders puede crear un segundo pedido y cobrar dos veces al cliente. Una clave de idempotencia convierte el reintento peligroso en uno seguro. El cliente genera un valor único (normalmente un UUID aleatorio) para cada operación lógica y lo envía en un encabezado como Idempotency-Key. El servidor registra la clave junto con el resultado de la primera solicitud; cualquier solicitud posterior con la misma clave devuelve el resultado registrado en lugar de volver a hacer el trabajo.
Los proveedores de pagos popularizaron la técnica, y el grupo de trabajo HTTP APIs del IETF ha redactado un encabezado estándar Idempotency-Key. En el momento de escribir esto no se ha publicado como RFC (el último borrador, de octubre de 2025, ha caducado), pero sus reglas reflejan la práctica habitual: una clave reutilizada con un cuerpo de solicitud distinto debería rechazarse con un 422, un reintento que llega mientras la primera solicitud aún se está ejecutando debería recibir un 409 Conflict para que nunca se ejecuten dos copias a la vez, y las claves están limitadas a un cliente y caducan tras un periodo publicado, a menudo 24 horas. El ejemplo práctico aplica cada regla.
Un pedido reintentado con una clave de idempotencia
Ejemplo práctico
Ejemplo práctico: el doble toque en el proceso de pago
Un cliente con una conexión móvil débil toca Hacer pedido. La app genera la clave 7f3c… y envía POST /v1/orders. El servicio de pedidos guarda la clave junto con un hash del cuerpo de la solicitud y, más adelante, la respuesta.
| Intento 1 | Se crea el pedido 1042 y se autoriza el pago; la clave, el hash del cuerpo y la respuesta 201 se guardan en la misma transacción de base de datos que el pedido. La respuesta se pierde cuando el teléfono pasa de wifi a datos móviles. |
|---|---|
| Intento 2 (reintento automático, misma clave) | El servicio encuentra la clave con el estado completado, comprueba que el hash del cuerpo coincide y devuelve el 201 guardado para el pedido 1042. Ni segundo pedido ni segundo cobro. |
| Intento 3 (el cliente vuelve a tocar mientras el intento 2 está en curso) | La app reutiliza la misma clave para el mismo carrito. Si la primera solicitud siguiera procesándose, el servicio respondería 409 y la app esperaría y reintentaría. |
| Un error de programación envía la misma clave con un carrito distinto | Los hashes del cuerpo no coinciden: el servicio responde 422 en lugar de devolver en silencio el pedido equivocado. |
| Pasadas 24 horas | La clave caduca y se elimina; la app nunca reintenta ese carrito antiguo. |
La clave debe generarse una vez por operación lógica (por cada envío del carrito), no por cada intento HTTP; una clave nueva en cada reintento anula su propósito.
Guardar la clave en la misma transacción que el pedido es lo que hace fiable la comprobación: una caída no puede dejar un pedido sin su clave, ni una clave sin su pedido.
La misma idea, aplicada a mensajes en lugar de solicitudes HTTP, es el consumidor idempotente de arquitectura dirigida por eventos.
def create_order(request, db):
key = request.headers.get("Idempotency-Key")
if not key:
return problem(400, "missing-idempotency-key")
body_hash = sha256(request.body)
with db.transaction():
row = db.find_idempotency(request.client_id, key)
if row and row.body_hash != body_hash:
return problem(422, "idempotency-key-reused")
if row and row.status == "in_progress":
return problem(409, "request-in-progress")
if row:
return row.saved_response # replay, do nothing new
db.insert_idempotency(request.client_id, key, body_hash, "in_progress")
order = db.insert_order(request.json)
response = created(order)
db.complete_idempotency(request.client_id, key, response)
return responseErrores que las máquinas pueden leer: RFC 9457
Los clientes necesitan errores sobre los que puedan actuar de forma programática, y las personas necesitan errores que puedan entender. En lugar de que cada equipo invente una forma de error, usa Problem Details for HTTP APIs, el RFC 9457 (julio de 2023), que sustituyó al RFC 7807 (2016). Un objeto problem details tiene el tipo de medio application/problem+json y cinco miembros estándar: type (un URI que identifica el tipo de problema y que puede enlazar con documentación), title (un resumen breve y legible de ese tipo), status (el código de estado HTTP), detail (una explicación de esta ocurrencia concreta) e instance (un URI que identifica esta ocurrencia). Los miembros de extensión pueden transportar detalles legibles por máquina, como qué campos no superaron la validación.
Dos reglas mantienen seguras las respuestas de error. El código del cliente debería ramificarse según type y status, nunca según la redacción de title o detail, que pueden traducirse o mejorarse. Y los errores no deben filtrar detalles internos: las trazas de pila, los fragmentos de SQL y los nombres de host internos ayudan más a los atacantes que a quienes llaman. Registra los detalles con un ID de correlación y devuelve el ID en su lugar.
{
"type": "https://api.fooddelivery.example/problems/outside-delivery-area",
"title": "Address is outside the delivery area",
"status": 422,
"detail": "Restaurant 7 delivers within 4 km; the address is 6.2 km away.",
"instance": "/v1/orders/attempts/9b1e",
"max_distance_km": 4,
"actual_distance_km": 6.2
}Hacer evolucionar una API sin romper a sus consumidores
Una API con consumidores es una promesa, y la ley de Hyrum (Sección 0.6) advierte de que alguien depende de cada comportamiento observable. Por eso el cambio se rige por un principio: haz cambios compatibles hacia atrás libremente y evita los que rompen la compatibilidad. Los consumidores ayudan siendo lectores tolerantes (tolerant readers) (un término que usa Martin Fowler): ignoran los campos que no entienden y leen solo lo que necesitan, de modo que los añadidos nunca los rompen. La tabla siguiente resume las reglas habituales; la misma lógica, aplicada a los datos almacenados y a los esquemas de eventos, aparece en evolución de esquemas.
Cuando un cambio incompatible es inevitable, versiona. Las estrategias habituales son una versión en la ruta de la URL (/v2/orders: explícita, fácil de enrutar y de almacenar en caché, pero fomenta saltos grandes y poco frecuentes), un encabezado personalizado o un parámetro del tipo de medio (Accept: application/vnd.fooddelivery.v2+json: mantiene estables las URL, pero es menos visible y más difícil de probar en un navegador), y versiones con fecha fijadas por cuenta de cliente, que algunos grandes proveedores de API usan para lanzar continuamente pequeños cambios incompatibles mientras cada cliente actualiza cuando decide. Las API internas entre servicios de una misma organización a menudo evitan las versiones por completo mediante el enfoque de expandir y contraer (expand and contract): añadir el nuevo campo o endpoint, migrar a todos los consumidores y después eliminar el antiguo. Sea cual sea la estrategia, anuncia las obsolescencias con antelación; los encabezados de respuesta Deprecation (RFC 9745, 2025) y Sunset (RFC 8594, 2019) permiten que un servidor indique a los clientes, de forma legible por máquina, que un endpoint está obsoleto y cuándo dejará de funcionar.
Cambios compatibles hacia atrás e incompatibles en una API
| Cambio | ¿Compatible? | Por qué |
|---|---|---|
| Añadir un campo opcional a la solicitud o un parámetro de consulta | Sí | Los clientes existentes no lo envían; el servidor debe aplicar un valor por defecto razonable. |
| Añadir un campo a una respuesta | Normalmente | Los lectores tolerantes lo ignoran; los clientes estrictos que rechazan campos desconocidos fallarán, así que documenta que los clientes deben tolerar los añadidos. |
| Añadir un nuevo endpoint o recurso | Sí | Nadie lo llama todavía. |
| Añadir un valor a una enumeración en una respuesta | Arriesgado | Los clientes con sentencias switch exhaustivas pueden fallar con el valor desconocido; documenta que pueden aparecer valores nuevos. |
| Hacer obligatorio un campo opcional de la solicitud | No | Los clientes existentes que lo omiten empiezan a fallar con un 400. |
| Eliminar o renombrar un campo, o cambiar su tipo o sus unidades | No | Los clientes que leen el campo antiguo no obtienen nada, u obtienen algo equivocado. |
| Cambiar el significado de un campo o de un código de estado | No | La ruptura más peligrosa: nada falla de forma visible, pero el comportamiento es incorrecto. |
| Endurecer la validación (longitud máxima más corta, formato más estricto) | No | Solicitudes que antes tenían éxito ahora fallan. |
Describir contratos: OpenAPI y AsyncAPI
Un contrato escrito en un formato legible por máquina puede alimentar la documentación, la generación de clientes, los stubs de servidor, los servidores simulados (mock), el análisis estático (linting) y las pruebas. OpenAPI es el estándar para las API HTTP. Empezó como la especificación Swagger, que SmartBear donó en 2015 a la recién creada OpenAPI Initiative, bajo la Linux Foundation, y que cambió de nombre en 2016; la versión 3.1 (2021) alineó sus esquemas con JSON Schema, y la versión 3.2 llegó en septiembre de 2025. AsyncAPI aplica la misma idea a las API basadas en mensajes: describe los canales (colas y topics), las operaciones que envían o reciben por ellos y los esquemas de los mensajes; la versión 3.0 se lanzó en diciembre de 2023.
Los equipos eligen entre primero el diseño (design-first) (escribir y revisar el contrato, y luego implementarlo) y primero el código (code-first) (generar el contrato a partir de código anotado). Primero el diseño convierte la API en una decisión deliberada que los consumidores pueden revisar antes de construir nada, y es adecuado para API públicas y entre equipos; primero el código mantiene sincronizados el contrato y la implementación con menos esfuerzo, y es adecuado para API internas que cambian con rapidez. En cualquier caso, guarda el contrato en el control de versiones, analízalo con un linter en el pipeline y ejecuta una comprobación de cambios incompatibles que compare cada versión propuesta con la publicada.
Pruebas de contrato y contratos dirigidos por el consumidor
Las pruebas de extremo a extremo que despliegan todos los servicios juntos son lentas, inestables y se vuelven más lentas a medida que se multiplican los servicios; sin embargo, las pruebas unitarias no pueden detectar que un proveedor ha cambiado un campo del que depende un consumidor. Las pruebas de contrato cubren ese hueco: cada parte se prueba de forma aislada frente a una descripción compartida de sus interacciones. En los contratos dirigidos por el consumidor, un patrón que Ian Robinson describió en 2006, cada consumidor declara las partes de la API del proveedor que realmente usa, y el proveedor ejecuta las expectativas de todos sus consumidores como parte de su propia compilación. Así el proveedor sabe exactamente qué campos no lee nadie (seguros de cambiar) y qué cambios romperían a un consumidor concreto.
Pact, creado en realestate.com.au en 2013, es la herramienta más conocida. Las pruebas del consumidor se ejecutan contra un proveedor simulado de Pact y registran en un archivo de contrato las solicitudes realizadas y las respuestas esperadas; el pipeline del proveedor reproduce esas solicitudes contra el proveedor real y comprueba las respuestas. Un broker almacena los contratos y los resultados de verificación, de modo que, antes de desplegar, un pipeline puede preguntar si esta versión de un servicio es compatible con las versiones de sus consumidores y proveedores que ya se ejecutan en producción. Las pruebas de contrato no sustituyen a unas pocas pruebas de humo de extremo a extremo, y comprueban la estructura en lugar del significado de negocio, pero permiten desplegar los servicios de forma independiente con confianza, que es precisamente el objetivo de separarlos.
Comprueba lo que has entendido
¿Por qué poner una cola entre dos servicios no elimina todo el acoplamiento entre ellos?
Elimina el acoplamiento temporal, pero ambos servicios siguen dependiendo del formato y del significado del mensaje, y del propio intermediario, así que un cambio en el mensaje todavía puede romper al receptor.
¿Por qué DELETE es idempotente aunque un segundo DELETE pueda devolver 404 en lugar de 204?
La idempotencia se refiere al efecto previsto en el servidor, no a la respuesta: tras uno o dos DELETE, el recurso ha desaparecido en cualquier caso.
Un cliente móvil genera una nueva clave de idempotencia cada vez que reintenta. ¿Qué sale mal?
El servidor ve cada reintento como una operación nueva, así que una respuesta perdida seguida de un reintento todavía puede crear un pedido duplicado; la clave debe pertenecer a la operación lógica, no al intento HTTP.
¿Por qué la paginación por cursor puede mostrar una lista estable cuando llegan pedidos nuevos mientras el usuario pagina, y la paginación por offset puede no hacerlo?
El cursor apunta justo después de un elemento concreto, así que las inserciones en otros puntos no desplazan la página siguiente; un offset cuenta filas, así que las filas nuevas empujan elementos de una página a otra y provocan duplicados u omisiones.
Fuentes y lecturas recomendadas
¿Tienes una pregunta sobre esta sección? ArchitectBot, el asistente de estudio con IA del curso, responde primero a partir del curso.
Pregunta: “Nuestro servicio de pedidos llama de forma síncrona a los servicios de pagos, de restaurantes y de despacho de repartidores. ¿Cuáles de esas llamadas deberían pasar a ser asíncronas y cómo mantenemos informado al cliente?”