Be Software Architecture Wise
Be Wise Apps

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ítulos
Solo 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

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

Una cadena síncrona en la que cada servicio espera al siguiente, comparada con una entrega asíncrona a través de una colaSíncrono: cada salto esperaApp clientesolicitudServ. pedidosbloqueado, esperasolicitudServicio de pagosLos tres activos a la vez; las latencias se suman, las disponibilidades se multiplicanAsíncrono: un broker guarda el mensajeServ. pedidosenvíaColaOrderPlaced esperacuando puedaServicio denotific.El emisor sigue; el receptor puede estar caído un rato sin que el emisor lo note
En una cadena síncrona, quien llama queda bloqueado y depende de todos los servicios posteriores; con un intermediario en medio, el emisor y el receptor ya no necesitan estar disponibles en el mismo momento.

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étodoUso típico¿Seguro?¿Idempotente?¿Se puede reintentar a ciegas?
GET, HEADLeer un recursoSíSíSí
PUTCrear o sustituir en un URI conocidoNoSíSí
DELETEEliminar un recursoNoSíSí (una segunda llamada puede devolver 404, pero el efecto es el mismo)
POSTCrear en una colección o ejecutar una acciónNoNoSolo con una clave de idempotencia u otra comprobación de duplicados
PATCHActualización parcialNoNo en generalSolo 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;
}
Un contrato gRPC para el servicio de despacho de repartidores de la plataforma de reparto de comida, con una llamada unaria y una de streaming del servidor (sintaxis de Protocol Buffers).

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

AspectoREST sobre HTTPgRPCGraphQLMensajería (colas y topics)
InteracciónSolicitud-respuesta síncronaSíncrona; unaria o streamingConsultas y mutaciones síncronas; las suscripciones envían datosAsíncrona; enviar y olvidar (fire-and-forget) o solicitud-respuesta mediante ID de correlación
ContratoDocumento OpenAPI (opcional pero habitual)Archivos .proto (obligatorios)Esquema (obligatorio)Documento AsyncAPI y esquemas de mensajes (opcionales pero recomendables)
Carga útilNormalmente texto JSONProtocol Buffers binarioJSONCualquiera: JSON, Avro, Protocol Buffers
Acoplamiento temporalSíSíSíNo: el intermediario almacena los mensajes
CachéExcelente: cachés HTTP estándar y CDNPoco incorporadoMás difícil: necesita consultas persistidas o cachés en el clienteNo procede
Soporte en navegadoresNativoNecesita un proxy (gRPC-Web)NativoNo directamente; mediante un backend o WebSockets
Mejor usoAPI públicas y de socios; CRUD orientado a recursosLlamadas internas entre servicios de alto volumen y baja latencia; streamingMuchos tipos de cliente con necesidades de datos distintas; agregar varios serviciosTrabajo que puede hacerse más tarde; fan-out a muchos consumidores; suavizar la carga
Principales trampasAPI conversadoras (chatty); convenciones incoherentesOpaco al depurar; disciplina con los números de campoResolvers N+1; costo de consulta sin límite; autorización a nivel de campoDuplicados, 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

Diagrama de secuencia: un pedido reintentado con la misma clave de idempotencia devuelve el resultado guardado en lugar de crear un segundo pedidoApp clienteAPI pedidosBase de datos1. POST /orders Clave: k1guarda pedido y clave k1 juntosconfirmado: pedido 1042se pierde el 201timeout: reint.2. POST /orders Clave: k1busca k1: ya hechoresp. guardada201 Created: pedido 1042
El primer intento tiene éxito, pero su respuesta se pierde; el reintento lleva la misma clave, así que la API devuelve el resultado guardado en lugar de crear el pedido 1043.

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 1Se 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 distintoLos hashes del cuerpo no coinciden: el servicio responde 422 en lugar de devolver en silencio el pedido equivocado.
Pasadas 24 horasLa 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 response
Tratamiento de una clave de idempotencia en el servidor (simplificado: una transacción de base de datos por solicitud; la restricción de unicidad sobre la clave impide que dos solicitudes simultáneas se ejecuten ambas).

Errores 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
}
Una respuesta problem details (HTTP 422, Content-Type: application/problem+json) para un pedido fuera de la zona de reparto.

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 consultaSíLos clientes existentes no lo envían; el servidor debe aplicar un valor por defecto razonable.
Añadir un campo a una respuestaNormalmenteLos 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 recursoSíNadie lo llama todavía.
Añadir un valor a una enumeración en una respuestaArriesgadoLos clientes con sentencias switch exhaustivas pueden fallar con el valor desconocido; documenta que pueden aparecer valores nuevos.
Hacer obligatorio un campo opcional de la solicitudNoLos clientes existentes que lo omiten empiezan a fallar con un 400.
Eliminar o renombrar un campo, o cambiar su tipo o sus unidadesNoLos 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 estadoNoLa 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)NoSolicitudes 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.

Siguiente · 6.6CAP y consistenciaQué significa la consistencia para los datos replicados: los modelos, de linealizable a eventual, lo que dice realmente el teorema CAP y lo que no dice, el compromiso cotidiano de latencia de PACELC, la aritmética de los quórums y cómo elegir la consistencia para cada caso de uso.Continuar →

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?”