Herman Primo.
Volver al inicio

Blog

Todas las publicaciones técnicas.

Explora contenido sobre backend, APIs, bases de datos, seguridad, rendimiento y arquitectura de software.

12 publicación(es) encontrada(s)

Página 1 de 1

Escribes una dirección en Google Maps… y segundos después ya tienes una ruta completa.
Arquitectura de Software

Escribes una dirección en Google Maps… y segundos después ya tienes una ruta completa.

Abres Google Maps. Escribes tu destino. Presionas buscar. Y en apenas unos segundos aparecen: 📍 Tu ubicación. 🛣️ Varias rutas posibles. ⏱️ El tiempo estimado. 🚦 El tráfico. 🚗 Rutas alternativas. 🚧 Avisos sobre incidentes o restricciones. Todo parece inmediato. Pero detrás de ese mapa hay mucho más que una imagen con calles. 👉 Hay sistemas procesando datos geográficos, tráfico, ubicaciones y rutas constantemente para responder una pregunta aparentemente sencilla: “¿Cómo llego hasta ahí?” 🧠 ¿Qué ocurre cuando buscas un lugar? Supongamos que escribes: Aeropuerto de Cancún Para una persona, ese texto tiene un significado claro. Pero una computadora necesita convertirlo en información que pueda procesar. Una de las primeras tareas consiste en identificar qué lugar estás buscando y asociarlo con una posición geográfica. Este proceso está relacionado con la geocodificación . Conceptualmente: "Aeropuerto de Cancún" ↓ Buscar lugar ↓ Identificar ubicación ↓ Latitud y longitud El resultado puede ser algo parecido a: 21.0365, -86.8771 Ahora el sistema ya no trabaja solamente con un nombre. Tiene un punto concreto sobre la Tierra. 📍 ¿Qué son la latitud y longitud? La mayoría de los sistemas de mapas representan ubicaciones mediante coordenadas. Por ejemplo: Latitud: 21.0365 Longitud: -86.8771 La latitud indica la posición de norte a sur. La longitud indica la posición de este a oeste. Con ambas coordenadas es posible representar una ubicación bastante precisa. Así, nombres como: Mi casa Aeropuerto Restaurante Hotel terminan convirtiéndose internamente en coordenadas que los sistemas pueden utilizar para realizar cálculos. 🔎 Pero buscar un lugar no siempre es tan sencillo Imagina que escribes: San José Puede existir más de un lugar con ese nombre. Entonces el sistema puede necesitar considerar otros datos: Tu ubicación actual. El país. El historial de búsqueda. Lugares cercanos. Popularidad. Contexto de la consulta. El objetivo es intentar determinar qué lugar querías encontrar realmente. Por eso una búsqueda geográfica no consiste únicamente en comparar texto. 🔄 También existe la geocodificación inversa Puede ocurrir el proceso contrario. Tu teléfono conoce tus coordenadas: 17.9894, -92.9475 pero necesitas mostrar una dirección comprensible. Entonces el sistema puede convertir: Coordenadas ↓ Calle ↓ Ciudad ↓ Estado ↓ País A este proceso se le conoce como geocodificación inversa . Es útil para mostrar información como: "Estás cerca de Avenida..." en lugar de obligarte a leer números de latitud y longitud. 🛰️ ¿Cómo sabe dónde estás? Tu teléfono puede determinar su ubicación utilizando varias fuentes. Por ejemplo: 🛰️ GPS. 📶 Redes Wi-Fi. 📡 Redes móviles. 📍 Información del dispositivo. Dependiendo del entorno, estas señales pueden combinarse para obtener una ubicación estimada. El sistema operativo entrega esa información a la aplicación si tiene los permisos necesarios. Entonces Google Maps puede trabajar con algo parecido a: Origen: 17.9894, -92.9475 Destino: 21.0365, -86.8771 Ahora ya conoce los dos extremos del problema. 🛣️ ¿Cómo representa Google Maps las calles? Aquí empieza una de las partes más interesantes. Para un sistema de rutas, una ciudad puede representarse como un enorme grafo . Un grafo está formado principalmente por: Nodos + Conexiones Podemos imaginar las intersecciones como nodos. Y las carreteras como conexiones. Por ejemplo: A ───── B │ │ │ │ C ───── D Cada letra puede representar una intersección. Cada línea representa una calle que permite viajar entre ellas. ⚙️ Las conexiones tienen información adicional Una carretera no es solamente: A → B También puede tener información asociada. Por ejemplo: Distancia: 2.4 km Velocidad estimada: 50 km/h Sentido: una dirección Peaje: no Tipo: avenida También pueden existir reglas como: No girar a la izquierda Carretera cerrada Acceso restringido Solo transporte público Toda esa información puede modificar qué rutas son válidas. 🚦 La mejor ruta no siempre es la más corta Imagina dos opciones. Ruta A 10 km Ruta B 13 km A simple vista podríamos elegir la A. Pero ahora agregamos tráfico: Ruta A 10 km 45 minutos Ruta B 13 km 25 minutos Entonces la ruta más larga puede ser mejor. Por eso los sistemas de navegación no buscan únicamente minimizar distancia. Pueden intentar minimizar: Tiempo estimado considerando las condiciones actuales. 🧠 El problema se parece a buscar un camino dentro de un grafo Conceptualmente tenemos: Origen ↓ Miles de posibles caminos ↓ Destino El sistema necesita encontrar una ruta conveniente entre ambos puntos. En informática existen algoritmos clásicos para resolver problemas de caminos, como: Dijkstra. A*. Pero una plataforma a escala global necesita muchas optimizaciones adicionales para responder rápidamente sobre redes viales gigantescas. La idea esencial sigue siendo: 👉 explorar posibles caminos y encontrar uno con un costo conveniente. 💰 ¿Qué significa “costo” en una ruta? En algoritmos de grafos, cada conexión puede tener un costo. Podría ser simplemente: Distancia Pero en navegación real puede representar una combinación de factores. Por ejemplo: Tiempo de viaje + Tráfico + Restricciones + Tipo de vía + Peajes Dependiendo de tus preferencias. Por ejemplo, si activas: Evitar autopistas el costo o la disponibilidad de ciertas carreteras puede cambiar. Si seleccionas: Evitar peajes el sistema busca rutas diferentes. 🚗 El medio de transporte cambia completamente el problema La ruta correcta depende de cómo estés viajando. No es lo mismo: 🚗 Automóvil. 🚶 Caminar. 🚲 Bicicleta. 🚌 Transporte público. Una carretera puede ser excelente para un automóvil pero imposible para un peatón. Una calle puede permitir bicicletas pero tener restricciones para vehículos. Entonces el sistema puede trabajar con redes y reglas diferentes dependiendo del modo seleccionado. 🚶 Una ruta caminando Si eliges caminar, puede considerar: Banquetas Cruces Puentes peatonales Senderos Escaleras que quizá no formen parte de una ruta para automóvil. 🚲 Una ruta en bicicleta Podría considerar: Ciclovías Pendientes Calles permitidas Infraestructura ciclista dependiendo de los datos disponibles. 🚌 Transporte público Aquí el problema se vuelve todavía más interesante. Ahora no basta con saber dónde están las calles. También necesitas: Rutas de autobús Paradas Horarios Estaciones Transbordos Entonces una ruta puede ser: Caminar 500 m ↓ Tomar autobús ↓ Viajar 7 paradas ↓ Cambiar de línea ↓ Caminar 300 m El backend necesita combinar múltiples tipos de información. 🚦 ¿Cómo sabe dónde hay tráfico? Una ruta puede ser correcta geográficamente y aun así ser terrible por tráfico. Por eso Google Maps puede utilizar diferentes fuentes de información para estimar cómo se están moviendo las carreteras. El sistema puede detectar que normalmente un tramo debería recorrerse en: 5 minutos pero actualmente está tardando: 20 minutos Eso indica que existe una condición diferente a la habitual. 📱 Los dispositivos pueden ayudar a estimar movimiento Los teléfonos y dispositivos que comparten cierta información de ubicación de manera agregada pueden contribuir a estimar cómo se mueve el tráfico. Imagina muchos dispositivos desplazándose sobre la misma avenida. Si normalmente avanzan a: 50 km/h pero ahora la mayoría se mueve a: 8 km/h el sistema puede inferir que existe congestión. Naturalmente, plataformas de este tipo también pueden combinar otras fuentes y modelos para mejorar las estimaciones. 🕒 También existe tráfico histórico No toda predicción depende únicamente de lo que está ocurriendo en este segundo. El sistema puede conocer patrones históricos. Por ejemplo: Lunes 8:00 AM ↓ Esta avenida normalmente tiene tráfico Mientras: Domingo 6:00 AM ↓ Normalmente está despejada Entonces el tiempo estimado puede combinar: Condiciones actuales + Patrones históricos + Características de la ruta Eso ayuda a estimar mejor cuánto tardarás. ⏱️ ¿Cómo calcula el tiempo estimado? Supongamos una ruta dividida en varios segmentos. Segmento A 5 minutos Segmento B 8 minutos Segmento C 4 minutos El sistema puede combinar esos tiempos: 5 + 8 + 4 = 17 minutos Pero la realidad es más compleja. Los tiempos pueden variar constantemente. Por eso el ETA, o Estimated Time of Arrival , puede actualizarse mientras conduces. 🚀 ¿Por qué cambia el tiempo durante el viaje? Empiezas con: Llegada: 15:40 Después: Llegada: 15:46 Y más adelante vuelve a: 15:42 No necesariamente significa que el sistema estuviera “equivocado”. Las condiciones pueden estar cambiando. Por ejemplo: Tráfico nuevo Accidente Carretera despejada Cambio de velocidad Ruta diferente El sistema recalcula continuamente utilizando información más reciente. 🔄 ¿Por qué puede ofrecer una ruta nueva mientras conduces? Imagina que empezaste con: Ruta A 30 minutos Unos minutos después ocurre un accidente. Ahora: Ruta A 50 minutos Pero existe otra alternativa: Ruta B 35 minutos El sistema vuelve a evaluar las posibilidades. Entonces puede mostrar: “Se encontró una ruta más rápida.” Conceptualmente: Ruta actual ↓ Condiciones cambian ↓ Recalcular ↓ Comparar alternativas ↓ Ofrecer nueva ruta 🚧 Accidentes, cierres y restricciones No toda modificación viene del tráfico. También pueden existir eventos como: Accidente Carretera cerrada Obras Inundación Restricción temporal Si esa información llega al sistema, puede modificar inmediatamente el grafo utilizado para calcular rutas. Por ejemplo: A ───── B X │ C Si una conexión deja de estar disponible, el algoritmo tiene que buscar otro camino. ↩️ Los sentidos de circulación también importan Imagina: Calle A pero solamente permite conducir: → No puedes tratarla como: ↔ El grafo debe representar esta restricción. Lo mismo ocurre con: No girar No retorno Solo vuelta derecha La ruta tiene que ser legal y físicamente posible, no solamente corta. 🗺️ ¿El teléfono descarga todo el mapa del mundo? No. Sería completamente innecesario. Tu dispositivo necesita principalmente información relacionada con la zona que estás viendo y utilizando. Cuando mueves el mapa: Zona actual ↓ Usuario desplaza ↓ Solicitar nueva información Se descargan los datos necesarios para representar esa nueva región. Esto permite trabajar con mapas enormes sin cargar todo el planeta en memoria. 🧩 El mapa puede dividirse en pequeñas secciones Los sistemas de mapas suelen dividir la superficie en tiles o fragmentos. Conceptualmente: ┌────┬────┬────┐ │ A │ B │ C │ ├────┼────┼────┤ │ D │ E │ F │ ├────┼────┼────┤ │ G │ H │ I │ └────┴────┴────┘ Si estás viendo: E quizá la aplicación también prepare: B D F H porque podrías desplazarte hacia ellas. Así la interfaz se siente fluida. 🔍 El nivel de zoom también cambia lo que necesitas Cuando estás muy alejado, quizá solo necesitas: Países Estados Carreteras principales Pero al acercarte: Calles Negocios Edificios Puntos de interés Mostrar absolutamente todo desde el principio sería innecesario. El sistema entrega más detalle conforme aumenta el zoom. 📍 Buscar lugares es otro problema diferente Cuando escribes: pizza Google Maps no solamente necesita encontrar coordenadas. También debe buscar negocios o puntos de interés relacionados. Puede considerar cosas como: Distancia Coincidencia de nombre Categoría Relevancia Horario entre otros factores. Entonces: Buscar un lugar y: Calcular una ruta son problemas diferentes que trabajan juntos. 🏪 Información de lugares Una ubicación puede tener datos como: Nombre Dirección Coordenadas Categoría Horario Teléfono Y posiblemente otra información asociada. Cuando seleccionas el lugar, esos datos pueden provenir de servicios distintos de los que calculan la ruta. La aplicación los presenta como si todo fuera una sola cosa. 🧠 Una sola pantalla puede utilizar muchos servicios Cuando ves una ruta, detrás pueden existir sistemas especializados en: Geocodificación ↓ Búsqueda de lugares ↓ Mapas ↓ Ruteo ↓ Tráfico ↓ ETA La interfaz integra todos esos resultados. Por eso una aplicación moderna puede parecer sencilla aunque internamente dependa de muchos componentes. 📡 ¿Todo se calcula desde el backend? No necesariamente todo. El dispositivo también realiza trabajo. Por ejemplo: Renderizar mapa Mostrar animaciones Dibujar ruta Actualizar interfaz Obtener ubicación Pero operaciones que requieren enormes cantidades de datos globales suelen apoyarse fuertemente en infraestructura backend. El teléfono no necesita tener almacenadas todas las calles, condiciones de tráfico y lugares del mundo. 🚘 ¿Qué pasa mientras conduces? Tu dispositivo puede enviar actualizaciones de ubicación periódicamente. Conceptualmente: Ubicación 1 ↓ Ubicación 2 ↓ Ubicación 3 ↓ Ubicación 4 La aplicación compara tu posición con la ruta. Si continúas correctamente: Seguir indicaciones Si te desvías: Ubicación ya no coincide ↓ Recalcular ↓ Nueva ruta Todo ocurre casi automáticamente. ↪️ ¿Cómo sabe cuándo decirte “gira a la derecha”? La ruta no es solamente una línea. Puede estar compuesta por instrucciones. Por ejemplo: Avanza 2 km ↓ Gira a la derecha ↓ Continúa 500 m ↓ Toma la salida La aplicación conoce la geometría de la ruta y tu posición aproximada. Entonces puede determinar cuándo estás cerca de una maniobra y mostrar la instrucción correspondiente. 📍 La ubicación nunca es perfectamente exacta Otro detalle importante: El GPS tiene margen de error. Tu teléfono podría estimar que estás en: Punto A cuando realmente estás algunos metros más adelante. En una carretera con varias calles paralelas esto puede crear problemas. Por eso las aplicaciones de navegación necesitan interpretar la ubicación y asociarla con una vía probable. Esto puede relacionarse con técnicas de map matching . 🛣️ ¿Qué es map matching? Supongamos que el GPS devuelve: • ligeramente fuera de la carretera. El sistema puede determinar: “Probablemente el usuario realmente está sobre esta vía.” Entonces ajusta conceptualmente: GPS aproximado ↓ Carretera más probable ↓ Posición utilizada para navegación Esto ayuda a que la ruta no salte constantemente entre calles cercanas. 📴 ¿Y los mapas sin conexión? Cuando descargas un mapa offline, parte de la información geográfica necesaria se almacena localmente. Entonces puedes utilizar ciertas funcionalidades sin conexión. Pero algunas capacidades pueden verse limitadas porque no tienes acceso a información actualizada. Por ejemplo: Mapa base ✅ Rutas posibles ✅ Tráfico en tiempo real ❌ Cambios recientes ❌ dependiendo de lo que se haya descargado y de la funcionalidad disponible. 📦 Caché también ayuda No tendría sentido descargar continuamente el mismo fragmento del mapa. Por eso puede utilizarse caché. Por ejemplo: Solicitar zona ↓ Guardar temporalmente ↓ Usuario vuelve ↓ Reutilizar Esto reduce: Transferencias. Latencia. Uso de datos. Y mejora la experiencia. 🌎 El verdadero desafío es la escala Calcular una ruta dentro de una ciudad ya es interesante. Pero ahora imagina hacerlo para: Millones de usuarios simultáneamente. Cada uno tiene: Origen diferente Destino diferente Horario diferente Medio de transporte diferente Tráfico diferente El backend debe responder rápidamente a todas esas combinaciones. 📈 Los datos también cambian Una red vial no es estática. Constantemente pueden existir: Nuevas carreteras Calles cerradas Negocios nuevos Cambios de sentido Obras Nuevos accesos La plataforma necesita mantener sus datos actualizados. Una ruta perfecta utilizando datos antiguos puede convertirse en una ruta incorrecta. 🔄 Actualizar mapas es un proceso continuo Conceptualmente: Nuevos datos ↓ Validación ↓ Procesamiento ↓ Actualizar información geográfica ↓ Disponible para usuarios Por eso mantener un mapa global actualizado es un problema de datos además de un problema de visualización. ⚠️ Error común: pensar que Maps solo “dibuja calles” La interfaz visual es solamente una pequeña parte. Para responder: ¿Cómo llego? el sistema puede necesitar resolver: ¿Dónde estás? ¿Dónde está el destino? ¿Qué caminos existen? ¿Qué caminos puedes utilizar? ¿Cuánto tardaría cada uno? ¿Qué tráfico hay? ¿Qué ruta conviene ahora? Todo eso ocurre antes de dibujar una simple línea azul sobre la pantalla. 🛠️ ¿Qué podemos aprender de Google Maps como desarrolladores? Este tipo de sistema deja varias ideas interesantes. ✔️ Modelar correctamente los datos puede hacer posible resolver problemas muy complejos. ✔️ Los grafos son fundamentales para representar redes. ✔️ El resultado “óptimo” depende de qué métrica quieras optimizar. ✔️ Los datos en tiempo real pueden cambiar decisiones previamente correctas. ✔️ Dividir grandes cantidades de información en pequeñas partes facilita su entrega. ✔️ Caché y procesamiento distribuido ayudan a reducir latencia. ✔️ Una interfaz sencilla puede esconder múltiples servicios especializados. 🧩 La realidad Cuando escribes: “Aeropuerto de Cancún” y presionas: Cómo llegar el sistema necesita transformar una frase en todo un problema computacional. Conceptualmente: Texto ↓ Lugar ↓ Coordenadas ↓ Red de carreteras ↓ Rutas posibles ↓ Tráfico y restricciones ↓ Tiempo estimado ↓ Mejor alternativa ↓ Mapa Y después continúa repitiendo parte del proceso mientras conduces. Porque la mejor ruta de hace cinco minutos puede no ser la mejor ruta ahora. 💬 Un mapa digital no solamente necesita saber dónde están las calles. Necesita entender cómo están conectadas, cuánto cuesta recorrerlas y cómo están cambiando las condiciones en ese momento. 👉 La próxima vez que Google Maps diga: “Se encontró una ruta más rápida” recuerda que detrás probablemente se volvió a resolver el mismo problema con información más reciente. 🔥 El backend no se ve, pero sin él, nada funciona.

26 ago 2026
Leer publicación
¿Cómo puede tu aplicación saber que ocurrió algo en otro sistema… sin estar preguntando todo el tiempo?
APIs

¿Cómo puede tu aplicación saber que ocurrió algo en otro sistema… sin estar preguntando todo el tiempo?

Imagina que tu aplicación necesita saber cuándo ocurre algo fuera de ella. Por ejemplo: 💳 Se confirma un pago. 📦 Cambia el estado de un pedido. 👤 Se registra un usuario en otro sistema. ☁️ Termina un procesamiento externo. Una forma de hacerlo sería preguntar constantemente: ¿Ya ocurrió? ¿Y ahora? ¿Ya? ¿Ya? Ese enfoque se conoce como polling . Funciona. Pero puede generar muchas peticiones innecesarias. 👉 Una alternativa mucho más eficiente es hacer que el otro sistema te avise automáticamente cuando ocurra algo importante. Ese es el principio detrás de los webhooks . 🧠 ¿Qué es un webhook? Un webhook es un mecanismo que permite que un sistema envíe una notificación HTTP a otro sistema cuando ocurre un evento. En lugar de que tu aplicación pregunte: ¿El pago ya fue aprobado? el proveedor puede decirte: El pago acaba de ser aprobado. Y enviar automáticamente esa información a tu servidor. Conceptualmente: Sistema externo ↓ Ocurre evento ↓ Webhook ↓ Tu backend Tu aplicación simplemente necesita estar preparada para recibir esa petición. 🌐 Polling vs webhook La diferencia puede verse fácilmente. Polling Tu aplicación ↓ ¿Cambió algo? ↓ No Tu aplicación ↓ ¿Cambió algo? ↓ No Tu aplicación ↓ ¿Cambió algo? ↓ Sí Tu sistema tiene que preguntar repetidamente. Webhook Tu aplicación ↓ Espera Sistema externo ↓ Ocurre evento ↓ Envía notificación ↓ Tu aplicación reacciona Aquí no necesitas realizar consultas continuamente. El otro sistema inicia la comunicación. ⚙️ ¿Cómo recibe un webhook tu aplicación? Primero necesitas crear un endpoint preparado específicamente para recibir estos eventos. Por ejemplo: POST /webhooks/pagos Después registras esa dirección en el proveedor externo. Conceptualmente: https://api.midominio.com/webhooks/pagos Cuando ocurre un evento importante, el proveedor realiza una petición HTTP hacia esa URL. 🚀 El flujo completo Supongamos que utilizas un proveedor externo para procesar pagos. El usuario realiza una compra. El flujo puede ser: Usuario ↓ Realiza pago ↓ Proveedor procesa ↓ Pago aprobado ↓ Proveedor envía webhook ↓ POST /webhooks/pagos ↓ Tu backend recibe evento ↓ Actualiza pedido El usuario no necesita estar esperando a que tu aplicación consulte constantemente al proveedor. 📦 ¿Qué información puede traer un webhook? Depende del proveedor. Pero podría enviar algo como: { "id": "evt_1842", "type": "payment.completed", "data": { "order_id": 1842, "status": "completed" } } Tu backend recibe el contenido. Después puede revisar: ¿Qué evento ocurrió? Por ejemplo: payment.completed Y ejecutar la lógica correspondiente. 💳 Ejemplo con un pago Supongamos que inicialmente tienes: Pedido #1842 Estado: pendiente El proveedor procesa el pago. Después envía: payment.completed Tu backend puede hacer: Webhook recibido ↓ Validar evento ↓ Buscar pedido 1842 ↓ Actualizar estado ↓ Pedido = pagado Y posteriormente podría iniciar otras tareas. Por ejemplo: Pedido pagado ├── Enviar correo ├── Preparar envío └── Generar factura 📦 Otro ejemplo: seguimiento de envíos Imagina que utilizas una empresa de paquetería. Tu sistema guarda: Pedido #500 Estado: enviado La paquetería podría enviarte eventos como: package.in_transit package.out_for_delivery package.delivered Entonces tu aplicación puede actualizar automáticamente: Enviado ↓ En tránsito ↓ En reparto ↓ Entregado sin estar preguntando constantemente por cada paquete. ☁️ Procesos que tardan mucho tiempo Los webhooks también son útiles cuando una operación tarda demasiado como para mantener una petición HTTP abierta. Por ejemplo: Generar video Procesar archivo Analizar documento Crear reporte Convertir imagen Tu aplicación puede iniciar el trabajo: POST /procesar y recibir: Procesamiento iniciado Después continúa haciendo otras cosas. Minutos más tarde, el sistema externo envía: processing.completed mediante webhook. 🧩 Esto permite procesos asíncronos En lugar de: Aplicación ↓ Solicitar procesamiento ↓ Esperar 5 minutos ↓ Respuesta puedes tener: Aplicación ↓ Solicitar procesamiento ↓ Continuar Y después: Servicio externo ↓ Proceso terminado ↓ Webhook ↓ Aplicación Esto es mucho más adecuado para operaciones largas. 🔐 Pero no puedes confiar en cualquier petición Aquí aparece una parte muy importante. Tu endpoint: POST /webhooks/pagos está preparado para recibir información. Pero ¿qué evita que otra persona haga una petición falsa? Por ejemplo: { "type": "payment.completed", "order_id": 1842 } Si tu backend confía únicamente en el cuerpo recibido, alguien podría intentar marcar pedidos como pagados sin haber realizado ningún pago. Por eso los webhooks deben validarse. 🛡️ Verificar la autenticidad del evento Muchos proveedores incluyen una firma criptográfica en la petición. Por ejemplo, mediante un header: Webhook-Signature: abc123... El proveedor y tu servidor comparten cierta información secreta. Tu backend utiliza esa información para comprobar que el mensaje realmente fue generado por el proveedor y que su contenido no fue modificado. Conceptualmente: Webhook recibido ↓ Leer firma ↓ Calcular firma esperada ↓ Comparar ↓ ¿Coincide? ├── Sí → procesar └── No → rechazar Este paso es crítico. ⚠️ HTTPS también es necesario Los webhooks deberían recibirse mediante: HTTPS y no mediante HTTP sin cifrado. Esto protege la comunicación mientras viaja por la red. Pero HTTPS y firma resuelven problemas diferentes. Podemos pensar: HTTPS ↓ Protege la comunicación Firma ↓ Ayuda a verificar el origen e integridad del evento Ambos son importantes. 🚀 ¿Qué pasa cuando el webhook llega correctamente? El proveedor necesita saber que tu servidor recibió el evento. Normalmente espera una respuesta HTTP exitosa. Por ejemplo: 200 OK o algún otro código exitoso según la integración. Conceptualmente: Proveedor ↓ Webhook ↓ Tu servidor ↓ Procesar ↓ 200 OK Con esa respuesta el proveedor puede considerar que la entrega fue aceptada. 💥 ¿Y si tu servidor está caído? Supongamos: Proveedor ↓ Webhook ↓ ❌ Tu servidor no responde El proveedor podría recibir: Timeout o: 500 Internal Server Error Muchos servicios implementan reintentos. Por ejemplo: Intento 1 → Error ↓ Esperar Intento 2 → Error ↓ Esperar más Intento 3 → Correcto Esto permite que una pequeña caída de tu servidor no haga que pierdas el evento inmediatamente. 🔁 Por eso un webhook puede llegar varias veces Este detalle es fundamental. Supongamos: Webhook llega ↓ Tu backend actualiza pedido ↓ Pero la respuesta 200 nunca llega al proveedor Desde el punto de vista del proveedor: No sé si lo recibió. Entonces puede enviarlo de nuevo. Ahora tu aplicación recibe: payment.completed por segunda vez. 👉 Por eso debes asumir que un webhook puede ser duplicado. 🔐 Aquí aparece nuevamente la idempotencia Supongamos que recibes: evento_id = evt_12345 La primera vez: ¿Ya procesé evt_12345? ↓ No ↓ Procesar ↓ Guardar evt_12345 La segunda: ¿Ya procesé evt_12345? ↓ Sí ↓ No volver a ejecutar efectos Así evitas situaciones como: Enviar dos correos Generar dos facturas Descontar inventario dos veces Un webhook confiable debe soportar duplicados. 🗄️ Guardar los eventos recibidos puede ser muy útil Una estrategia común consiste en registrar información como: event_id tipo fecha payload estado Por ejemplo: evt_12345 payment.completed 2026-08-15 procesado Esto permite: Detectar duplicados. Investigar errores. Auditar eventos. Reprocesar fallos. Saber qué ocurrió. Cuando una integración falla en producción, este historial puede ser extremadamente útil. ⚡ Conviene responder rápido Otro punto importante: No siempre deberías ejecutar toda la lógica pesada antes de responder al proveedor. Imagina: Webhook ↓ Generar factura ↓ Enviar correo ↓ Actualizar inventario ↓ Procesar reporte ↓ Esperar 15 segundos ↓ 200 OK El proveedor podría asumir que tu endpoint tardó demasiado y considerar la entrega fallida. Incluso podría hacer un reintento. 🚀 Una estrategia mejor Puedes hacer: Webhook recibido ↓ Validar firma ↓ Guardar evento ↓ Enviar a cola ↓ 200 OK Y después: Worker ↓ Procesar evento ↓ Actualizar datos ↓ Enviar correo ↓ Generar factura Así confirmas rápidamente la recepción y continúas el trabajo de forma asíncrona. 📬 Webhooks y colas funcionan muy bien juntos Conceptualmente: Proveedor ↓ Webhook ↓ Tu API ↓ Cola de mensajes ↓ Worker ↓ Procesamiento Esto permite desacoplar: Recepción del evento de: Procesamiento del evento Si el procesamiento tarda o falla, no necesitas mantener al proveedor esperando. ⚠️ Pero responder 200 demasiado pronto también puede ser peligroso Supongamos: Recibir webhook ↓ 200 OK ↓ Intentar guardar ↓ ❌ Base de datos falla Ahora el proveedor cree que todo está correcto. Pero tú perdiste el evento. Por eso una estrategia segura suele ser confirmar después de haber almacenado de forma confiable la información necesaria. Por ejemplo: Recibir ↓ Validar ↓ Persistir evento ↓ 200 OK ↓ Procesar después Así aunque el worker falle posteriormente, el evento ya quedó registrado. 🧩 ¿Qué pasa si el evento llega desordenado? Este es otro problema real. Imagina: Evento A: pedido_enviado Evento B: pedido_entregado Normalmente esperamos: Enviado ↓ Entregado Pero por problemas de red o reintentos podrías recibir: Entregado ↓ Enviado Si actualizas el estado ciegamente, podrías terminar regresando el pedido a un estado anterior. 🧠 No siempre puedes asumir el orden Tu procesamiento puede necesitar considerar: Timestamp del evento. Versión. Estado actual. Secuencia. Reglas del negocio. Por ejemplo: Estado actual = entregado Evento recibido = enviado Tal vez debas ignorarlo porque representa información anterior. Esto depende de la integración. ⏰ Los webhooks también pueden llegar tarde Un evento puede ocurrir a: 10:00 pero llegar a: 10:05 por problemas de red o reintentos. Por eso no siempre debes interpretar: Hora de recepción como: Hora en que ocurrió el evento Si el proveedor incluye un timestamp del evento, puede ser útil conservarlo. 💳 Un webhook tampoco siempre debe ser tu única fuente de verdad Imagina que recibes: payment.completed Dependiendo del nivel de riesgo de la operación, tu sistema puede decidir verificar adicionalmente el estado directamente con el proveedor. Por ejemplo: Webhook recibido ↓ Verificar firma ↓ Consultar operación al proveedor ↓ Confirmar estado ↓ Actualizar pedido No siempre es necesario. Pero en operaciones críticas puede utilizarse como capa adicional de validación. 🔎 También existe el problema del replay Supongamos que un atacante obtiene una petición válida previamente enviada. Después intenta reenviarla varias veces. Aunque la firma sea correcta, el evento es antiguo. Por eso algunos esquemas de firma también incluyen información temporal. El backend puede comprobar: Firma válida ✅ Timestamp reciente ✅ Evento no procesado ✅ antes de aceptarlo. 🧠 Webhook no es exactamente lo mismo que WebSocket Los nombres pueden confundir. Un webhook normalmente es: Servidor A ↓ Petición HTTP puntual ↓ Servidor B Un WebSocket mantiene una conexión bidireccional abierta: Cliente ↔ Servidor Los webhooks son especialmente útiles para comunicación servidor a servidor basada en eventos. No son una conexión permanente. 🧩 Tampoco es lo mismo que polling Podemos resumir: Polling Yo te pregunto constantemente. Webhook Tú me avisas cuando algo ocurre. WebSocket Mantenemos una conexión abierta para comunicarnos. Cada uno resuelve problemas diferentes. 🚀 Ejemplo completo con ecommerce Imagina este proceso: Usuario crea pedido ↓ Pedido = pendiente ↓ Redirigir a proveedor de pagos El proveedor procesa el pago. Después: Proveedor ↓ payment.completed ↓ POST /webhooks/payments Tu backend: 1. Recibe evento 2. Verifica firma 3. Revisa event_id 4. Guarda evento 5. Responde 200 6. Envía procesamiento a cola Después el worker: 7. Busca pedido 8. Confirma que no fue procesado 9. Marca pedido como pagado 10. Reserva o actualiza inventario 11. Genera factura 12. Envía confirmación Ahora tienes un flujo mucho más robusto. ⚠️ Error común: confiar en el frontend para confirmar pagos Imagina que después de pagar el proveedor redirige al navegador hacia: /compra-exitosa Y el frontend dice al backend: "El pago fue exitoso." Eso no debería ser suficiente para marcar una compra como pagada. El usuario controla el navegador. Podría intentar acceder directamente a esa URL. La confirmación debería venir desde una fuente confiable, como el proveedor mediante un webhook o una consulta verificada desde el backend. 🔄 ¿Qué ocurre si tu aplicación necesita reconsultar eventos? Aunque los webhooks reduzcan muchísimo el polling, algunos sistemas combinan ambas estrategias. Por ejemplo: Webhook = mecanismo principal pero también: Proceso periódico ↓ Buscar inconsistencias ↓ Consultar proveedor Esto puede funcionar como mecanismo de reconciliación si algún evento se perdió o quedó sin procesar. 🧾 Reconciliación Imagina: Proveedor: Pago 100 = completado Tu sistema: Pago 100 = pendiente Un proceso periódico puede detectar esa diferencia y corregirla. Esto es especialmente importante en sistemas financieros. Los webhooks son muy útiles. Pero diseñar pensando en recuperación y reconciliación aumenta la confiabilidad. 🔍 Observabilidad También necesitas saber qué está ocurriendo con tus webhooks. Conviene medir: Eventos recibidos Eventos válidos Eventos rechazados Duplicados Errores de procesamiento Tiempo de procesamiento Por ejemplo: Webhook payment.completed ↓ 500 errores en 10 minutos debería llamar inmediatamente la atención. 🚨 Alertas Puedes generar alertas cuando: Aumentan errores o: Se acumulan eventos pendientes o: No llegan webhooks durante demasiado tiempo si normalmente existe tráfico constante. Así puedes detectar fallos antes de que los usuarios los reporten. 🧪 ¿Qué deberías probar? No pruebes únicamente: Webhook válido ↓ 200 OK También escenarios como: Firma inválida Evento duplicado Evento fuera de orden Payload inválido Base de datos caída Worker caído Procesamiento lento Evento antiguo Los casos difíciles son precisamente los que suelen aparecer en producción. 🛠️ Buenas prácticas ✔️ Utiliza HTTPS. ✔️ Verifica la firma o mecanismo de autenticidad proporcionado por el proveedor. ✔️ No confíes únicamente en el contenido del payload. ✔️ Guarda un identificador único del evento. ✔️ Diseña el procesamiento para soportar duplicados. ✔️ Registra los eventos recibidos. ✔️ Responde rápidamente después de asegurar la recepción. ✔️ Utiliza colas para trabajos pesados. ✔️ Considera que los eventos pueden llegar tarde o fuera de orden. ✔️ Implementa monitoreo y alertas. 🧩 La realidad La idea de un webhook parece sencilla: Ocurre algo ↓ Enviar POST Pero construir una integración confiable requiere pensar en muchas más cosas. ¿Quién envió el evento? ¿Ya lo procesé? ¿Y si llega dos veces? ¿Y si llega tarde? ¿Y si mi servidor está caído? ¿Y si el procesamiento falla? Ese es el verdadero reto. Los webhooks permiten que diferentes sistemas reaccionen a eventos sin estar consultándose constantemente. Pero la confiabilidad depende de cómo recibes, validas y procesas esos eventos. 💬 Una buena integración no necesita preguntar continuamente si algo cambió. Puede estar preparada para recibir el cambio, validarlo y reaccionar cuando ocurra. 👉 La próxima vez que un pago cambie automáticamente de "pendiente" a "confirmado" sin que actualices manualmente la página, probablemente haya existido algún mecanismo basado en eventos trabajando detrás. 🔥 El backend no se ve, pero sin él, nada funciona.

25 ago 2026
Leer publicación
Cuando una aplicación pide /usuarios/25… ¿cómo sabe la API qué debe hacer?
APIs

Cuando una aplicación pide /usuarios/25… ¿cómo sabe la API qué debe hacer?

Abres una aplicación. Consultas tu perfil. Ves tus pedidos. Actualizas una dirección. Creas un nuevo registro. Cada una de esas acciones puede provocar una petición distinta hacia el backend. Por ejemplo: GET /usuarios/25 GET /usuarios/25/pedidos POST /pedidos PATCH /usuarios/25 Pero... 👉 ¿Qué representan realmente esas direcciones? Son endpoints . Y son una de las formas principales en las que una API organiza el acceso a sus recursos. 🧠 ¿Qué es un endpoint? Un endpoint es un punto específico dentro de una API al que un cliente puede enviar una petición. Normalmente combina: Método HTTP + Ruta Por ejemplo: GET /productos puede significar: 👉 Obtener una lista de productos. Mientras que: POST /productos puede utilizarse para crear uno nuevo. La ruta es la misma: /productos pero la operación cambia dependiendo del método HTTP utilizado. ⚙️ ¿Qué representa realmente una ruta? En una API REST es común organizar las rutas alrededor de recursos . Por ejemplo: /usuarios /productos /pedidos /pagos Cada uno representa un tipo de información que existe dentro del sistema. Después, los métodos HTTP indican qué queremos hacer con esos recursos. Por ejemplo: GET /productos puede significar: Consultar productos Mientras que: POST /productos puede significar: Crear producto Y: DELETE /productos/25 puede indicar: Eliminar producto 25 Esto hace que la API sea más predecible. 📦 Recursos y colecciones Una ruta como: /productos normalmente representa una colección . Es decir: Producto 1 Producto 2 Producto 3 Producto 4 ... Mientras que: /productos/25 representa un recurso específico. En este caso: Producto con ID 25 Así podemos tener: GET /productos ↓ Obtener colección y: GET /productos/25 ↓ Obtener un producto 🚀 Un ejemplo completo Supongamos que tenemos una tienda en línea. Cuando el usuario entra al catálogo, el frontend podría enviar: GET /productos El backend responde con una lista. Después el usuario selecciona el producto 42. Entonces: GET /productos/42 Cuando agrega productos y crea un pedido: POST /pedidos Y si posteriormente consulta ese pedido: GET /pedidos/850 Cada endpoint representa una entrada concreta hacia una parte del sistema. 🧩 ¿Qué ocurre dentro del backend? Cuando llega una petición como: GET /usuarios/25 el servidor necesita encontrar qué parte del código debe procesarla. Conceptualmente: Request ↓ Router ↓ Controller ↓ Service ↓ Base de datos ↓ Response La arquitectura exacta depende del framework. Pero normalmente existe un componente de enrutamiento que revisa: Método = GET Ruta = /usuarios/25 y decide qué función debe ejecutarse. 🛣️ El router El router funciona como una especie de mapa. Puede tener reglas conceptualmente parecidas a: GET /usuarios/:id → obtenerUsuario() POST /usuarios → crearUsuario() DELETE /usuarios/:id → eliminarUsuario() Cuando llega: GET /usuarios/25 el router reconoce que: 25 corresponde al parámetro: id y envía la petición hacia la lógica correspondiente. 🔢 Parámetros de ruta Los valores que aparecen dentro de una URL pueden utilizarse para identificar recursos. Por ejemplo: GET /usuarios/25 Aquí: 25 es un parámetro de ruta. Podríamos expresarlo como: /usuarios/{id} Entonces: id = 25 Otro ejemplo: GET /productos/900 significa: id = 900 Estos parámetros permiten crear rutas dinámicas sin definir un endpoint diferente para cada registro. 🧬 Recursos relacionados También podemos representar relaciones. Por ejemplo: GET /usuarios/25/pedidos Esto puede interpretarse como: 👉 Obtener los pedidos pertenecientes al usuario 25. Conceptualmente: Usuario 25 ↓ Pedidos Entonces la URL ya comunica bastante información por sí sola. Eso ayuda a que la API sea intuitiva. ⚠️ Pero no conviene anidar demasiado Podríamos terminar creando algo como: /usuarios/25/pedidos/80/productos/15/comentarios/2 Aunque técnicamente puede funcionar, empieza a ser difícil de mantener y entender. Muchas veces una ruta más directa puede ser mejor. Por ejemplo: /pedidos/80 o: /pedidos/80/productos La estructura debe representar relaciones útiles sin convertir la URL en una cadena interminable. 🔎 Query Parameters No toda la información debe ir dentro de la ruta. También existen los query parameters . Por ejemplo: GET /productos?categoria=laptops Aquí: categoria=laptops no identifica necesariamente un producto específico. Sirve para filtrar la colección. También podríamos tener: GET /productos?pagina=2 o: GET /productos?orden=precio Los query parameters suelen utilizarse para: Filtros. Ordenamiento. Paginación. Búsquedas. Opciones adicionales. 🚀 Un ejemplo con filtros Supongamos: GET /productos devuelve todos los productos. Pero queremos solamente los disponibles. Podríamos hacer: GET /productos?disponible=true Después queremos ordenarlos: GET /productos?disponible=true&orden=precio Así mantenemos el recurso: /productos y utilizamos parámetros para modificar la consulta. 🧠 Los métodos HTTP Los métodos HTTP ayudan a expresar qué tipo de acción se quiere realizar. Los más comunes son: GET POST PUT PATCH DELETE Cada uno tiene un propósito diferente. 🔎 GET Se utiliza principalmente para consultar información. Por ejemplo: GET /usuarios o: GET /usuarios/25 Una petición GET debería limitarse a recuperar datos. No debería utilizarse para modificar información importante. ➕ POST Suele utilizarse para crear recursos. Por ejemplo: POST /usuarios con un cuerpo: { "nombre": "Herman", "email": "correo@email.com" } El backend procesa la información y crea el nuevo usuario. ✏️ PUT PUT suele utilizarse para reemplazar o actualizar un recurso completo según el contrato de la API. Por ejemplo: PUT /usuarios/25 podría enviar toda la representación actualizada del usuario. 🩹 PATCH PATCH suele utilizarse cuando queremos actualizar solamente ciertos campos. Por ejemplo: PATCH /usuarios/25 con: { "nombre": "Nuevo nombre" } Solo se modifica esa propiedad. 🗑️ DELETE Se utiliza para eliminar un recurso. Por ejemplo: DELETE /usuarios/25 indica que queremos eliminar el usuario 25. El backend todavía debe verificar si esa operación está permitida. ⚠️ El método no sustituye las validaciones Que alguien envíe: DELETE /usuarios/25 no significa que el backend tenga que eliminarlo inmediatamente. Primero puede verificar: ¿Existe? ↓ ¿Usuario autenticado? ↓ ¿Tiene permisos? ↓ ¿Puede eliminar este recurso? Los endpoints representan operaciones. Pero las reglas de seguridad y negocio continúan siendo responsabilidad del backend. 🔐 Un endpoint también debe controlar acceso Imagina: GET /usuarios/25 El servidor encuentra al usuario 25. Pero todavía debe preguntarse: ¿Quién está haciendo la petición? Tal vez ese endpoint solamente pueda ser utilizado por: Usuario 25 o por un administrador. Entonces el flujo podría ser: Request ↓ Autenticación ↓ Autorización ↓ Buscar usuario ↓ Responder Un endpoint bien diseñado no solamente encuentra datos. También protege quién puede utilizarlos. 📥 El cuerpo de la petición Algunos endpoints necesitan recibir información adicional. Por ejemplo: POST /pedidos podría enviar: { "productos": [ { "id": 25, "cantidad": 2 } ] } La ruta indica: /pedidos El método indica: POST Y el cuerpo contiene: Datos del nuevo pedido Estas tres partes trabajan juntas. 📤 La respuesta también forma parte del contrato El frontend necesita saber qué puede esperar. Por ejemplo: GET /productos/25 puede responder: { "id": 25, "nombre": "Laptop", "precio": 18000 } Pero si no existe: 404 Not Found Si no tiene permisos: 403 Forbidden Si ocurre un problema interno: 500 Internal Server Error Diseñar buenos endpoints también implica definir respuestas consistentes. 🚦 Los códigos HTTP ayudan a comunicar resultados Algunos códigos comunes son: 200 OK 201 Created 204 No Content 400 Bad Request 401 Unauthorized 403 Forbidden 404 Not Found 409 Conflict 500 Internal Server Error Por ejemplo: POST /usuarios si crea correctamente el recurso podría devolver: 201 Created Esto permite que el cliente entienda qué ocurrió sin depender únicamente del contenido de la respuesta. ⚠️ Error común: rutas como nombres de funciones Es común encontrar APIs con rutas como: /obtenerProductos /crearProducto /eliminarProducto /actualizarProducto Funcionan. Pero en un diseño REST suele ser más predecible utilizar: /productos y dejar que el método indique la acción. Por ejemplo: GET /productos POST /productos PATCH /productos/25 DELETE /productos/25 Esto hace que el diseño sea más uniforme. 🚫 Otro ejemplo poco consistente Imagina: GET /getUsers POST /new-product DELETE /removeOrder/25 PATCH /editCustomer/10 Cada endpoint utiliza una convención diferente. Un desarrollador tendría que memorizar cada ruta. Una estructura consistente sería más fácil: GET /usuarios POST /productos DELETE /pedidos/25 PATCH /clientes/10 La predictibilidad importa. 🧩 Sustantivos en lugar de verbos En REST normalmente las rutas representan recursos. Por eso suele preferirse: /productos en lugar de: /obtenerProductos porque el método HTTP ya representa la acción: GET Entonces: GET + /productos ya significa: Obtener productos ⚠️ Pero REST tampoco significa que nunca pueda haber acciones Hay operaciones que no encajan perfectamente en CRUD. Por ejemplo: POST /pedidos/25/cancelacion o alguna variante equivalente. La idea no es forzar artificialmente todo. El objetivo es mantener una interfaz clara y coherente. Las reglas REST son una guía de diseño, no una competencia por crear la URL más "pura". 🧱 CRUD y endpoints Muchas APIs comienzan con operaciones CRUD: Create Read Update Delete que pueden mapearse a: POST GET PUT/PATCH DELETE Por ejemplo: POST /productos ↓ Create GET /productos/25 ↓ Read PATCH /productos/25 ↓ Update DELETE /productos/25 ↓ Delete Esto crea una relación fácil de entender. 🔢 ¿Qué pasa con la versión de una API? Con el tiempo, una API puede cambiar. Por eso algunas utilizan rutas como: /api/v1/productos Y más adelante: /api/v2/productos El versionado ayuda a introducir cambios importantes sin romper inmediatamente a clientes que todavía utilizan la versión anterior. No todas las APIs necesitan exactamente esta estrategia, pero es una práctica común. 🌐 El frontend no necesita conocer el código interno Supongamos que el backend cambia completamente. Antes: Controller ↓ MySQL Después: Controller ↓ Service ↓ PostgreSQL ↓ Redis Si el contrato sigue siendo: GET /productos/25 y la respuesta mantiene el mismo formato, el frontend no debería necesitar saber que la implementación cambió. Este es uno de los beneficios más importantes de una API. 🤝 El endpoint funciona como un contrato El cliente necesita conocer: Ruta Método Datos de entrada Respuesta Posibles errores Por ejemplo: POST /usuarios puede establecer: Entrada: nombre email contraseña Respuesta: id nombre email Mientras ese contrato se mantenga, el equipo backend puede modificar muchos detalles internos. 📚 Por eso la documentación importa Una buena documentación debería explicar cada endpoint. Por ejemplo: GET /productos/{id} Parámetros id: Identificador del producto Respuesta { "id": 25, "nombre": "Laptop" } Errores 404 → producto no encontrado 401 → usuario no autenticado Esto hace que consumir la API sea mucho más sencillo. 🧪 Los endpoints también deben probarse No basta con probar: GET /productos/25 ↓ 200 OK También deberías comprobar: GET /productos/999999 ↓ 404 o: GET /usuarios/25 ↓ Usuario sin permisos ↓ 403 Y: POST /productos ↓ Datos inválidos ↓ 400 Una API tiene que comportarse correctamente tanto en escenarios exitosos como en errores. 📊 Un endpoint también puede necesitar paginación Imagina: GET /productos y tienes: 2,000,000 productos No deberías devolverlos todos en una sola respuesta. Puedes usar: GET /productos?page=1&limit=50 Entonces: /productos sigue siendo el recurso. Los parámetros controlan cómo queremos consultarlo. 🔎 Búsquedas También podrías tener: GET /productos?search=laptop El endpoint continúa representando productos. Simplemente estamos aplicando una condición. Esto suele ser más limpio que crear: /buscarProductoLaptop o rutas distintas para cada búsqueda. 📈 Ordenamiento Podríamos utilizar: GET /productos?sort=precio o: GET /productos?sort=-precio según la convención elegida. La idea es mantener una estructura consistente. 🛠️ Buenas prácticas ✔️ Utiliza nombres claros para los recursos. ✔️ Mantén una convención consistente entre endpoints. ✔️ Utiliza correctamente los métodos HTTP. ✔️ Usa parámetros de ruta para identificar recursos. ✔️ Utiliza query parameters para filtros, ordenamiento y paginación cuando tenga sentido. ✔️ Evita anidaciones excesivamente profundas. ✔️ Define respuestas y códigos HTTP consistentes. ✔️ Valida siempre la información recibida. ✔️ Protege cada endpoint con autorización cuando corresponda. ✔️ Documenta claramente el contrato. 🧩 La realidad Cuando el frontend envía: GET /usuarios/25/pedidos no está diciendo cómo debe funcionar internamente el backend. No sabe: Qué controlador existe Qué servicio se ejecuta Qué ORM utilizas Qué consulta SQL se genera Qué base de datos tienes Solo conoce una interfaz. El backend recibe la petición, interpreta la ruta y ejecuta toda la lógica necesaria detrás. Por eso los endpoints son una pieza fundamental en el diseño de APIs. 💬 Una API bien diseñada no solamente funciona. También permite que alguien pueda mirar: GET /usuarios/25/pedidos y tener una buena idea de lo que va a recibir. La consistencia hace que una API sea mucho más fácil de aprender, mantener e integrar. 👉 Cuando diseñes un endpoint, intenta pensar desde la perspectiva de quien va a consumirlo. Si necesita memorizar reglas completamente diferentes para cada ruta, probablemente todavía exista espacio para simplificar la API. 🔥 El backend no se ve, pero sin él, nada funciona.

24 ago 2026
Leer publicación
Tu formulario dice que el dato es válido… ¿pero qué pasa si alguien se salta el formulario?
Seguridad

Tu formulario dice que el dato es válido… ¿pero qué pasa si alguien se salta el formulario?

Creas un formulario. El usuario escribe su correo. El frontend verifica que tenga un formato correcto. Impide enviar campos vacíos. Limita ciertos valores. Deshabilita botones cuando algo no cumple las reglas. Todo parece estar protegido. Pero hay un detalle muy importante: 👉 El usuario no está obligado a utilizar tu interfaz. Puede abrir las herramientas del navegador. Modificar una petición. Llamar directamente a tu API. Utilizar Postman. Crear un script. O incluso construir su propio cliente. Por eso existe una regla fundamental en backend: ⚠️ Nunca confíes ciegamente en los datos que llegan desde el cliente. 🧠 ¿Qué es la validación del lado del servidor? La validación del lado del servidor consiste en comprobar nuevamente los datos cuando llegan al backend. Aunque el frontend ya los haya validado antes. Por ejemplo, una petición podría intentar enviar: edad: -200 email: "hola" precio: -500 rol: "admin" El backend debe decidir si esos valores son válidos antes de procesarlos o almacenarlos. No debería asumir que son correctos solo porque vienen desde tu propia aplicación. ⚙️ ¿Por qué validar dos veces? Porque frontend y backend cumplen responsabilidades diferentes. El frontend valida principalmente para mejorar la experiencia. Por ejemplo: Correo inválido puede mostrarse inmediatamente mientras el usuario escribe. Eso evita enviar una petición innecesaria. Pero esa validación puede eliminarse, modificarse o ignorarse. Por eso el flujo correcto suele ser: Usuario ↓ Frontend valida ↓ Petición HTTP ↓ Backend valida otra vez ↓ Lógica de negocio ↓ Base de datos Podemos resumirlo así: 👉 El frontend ayuda al usuario. 👉 El backend protege al sistema. 🚀 Un ejemplo real: una tienda en línea Supongamos que el frontend envía esta información: { "producto_id": 25, "cantidad": 2, "precio": 500 } A simple vista parece correcto. Pero un usuario podría interceptar la petición y cambiarla: { "producto_id": 25, "cantidad": 2, "precio": 1 } Si el backend confía directamente en: precio = 1 podría terminar procesando una operación manipulada. El problema no está en que el formulario haya fallado. El problema está en que el servidor confió en un dato que nunca debió considerar autoritativo. 💳 Los valores importantes deben venir del servidor En un ecommerce, el cliente puede enviar: producto_id = 25 cantidad = 2 Pero el backend debería obtener por sí mismo información sensible como: precio descuento real impuestos disponibilidad Por ejemplo: Cliente: producto_id = 25 cantidad = 2 Después: Backend ↓ Consultar producto 25 ↓ Precio real = $500 ↓ Calcular total Así el cliente no decide cuánto cuesta el producto. Solo indica qué desea comprar. 🔐 Otro ejemplo: roles Imagina un formulario de registro. El frontend normalmente envía: { "nombre": "Herman", "email": "usuario@email.com" } Pero alguien modifica la petición: { "nombre": "Herman", "email": "usuario@email.com", "rol": "admin" } Si el backend guarda automáticamente todos los campos recibidos, podría existir un problema muy serio. Por eso el servidor debe controlar qué propiedades puede modificar cada usuario. Por ejemplo: Campos permitidos: nombre email telefono Y rechazar o ignorar: rol es_admin permisos saldo cuando no corresponde. ⚠️ Aquí aparece el problema de Mass Assignment Un error común ocurre cuando el backend toma directamente todo el objeto recibido y lo guarda. Conceptualmente: request.body ↓ guardar directamente Supongamos que el modelo contiene: nombre email rol activo saldo Pero el formulario solamente muestra: nombre email Eso no significa que un atacante no pueda enviar los otros campos. Por eso es mucho más seguro definir explícitamente qué propiedades pueden recibirse. 🧩 Validar no significa solo revisar formatos Cuando hablamos de validación, muchas personas piensan únicamente en: ¿El email tiene @? ¿La edad es un número? ¿El campo está vacío? Pero existen varios tipos de validación. Por ejemplo: Validación de tipo edad = número Validación de formato email = formato válido Validación de rango cantidad >= 1 Validación de longitud nombre <= 100 caracteres Validación de negocio ¿Hay suficiente inventario? Validación de permisos ¿Este usuario puede modificar este recurso? Todas son necesarias en diferentes contextos. 🚨 Formato válido no significa dato permitido Supongamos: edad = 25 Es un número válido. Pero ahora: cantidad = 999999999 También es un número válido. Eso no significa que tu aplicación deba aceptarlo. El backend puede establecer reglas como: cantidad mínima = 1 cantidad máxima = 100 Por eso validar el tipo de dato no es suficiente. También necesitas validar su significado dentro del negocio. 🧠 Validaciones de negocio Imagina una aplicación de reservas. El usuario envía: fecha = 15 de agosto hora = 10:00 La fecha tiene formato correcto. La hora también. Pero el backend todavía debe comprobar: ¿Existe ese horario? ¿Está disponible? ¿Ya pasó la fecha? ¿El usuario puede reservar? El dato es sintácticamente correcto. Pero podría no ser válido para la operación. 🔍 Validación y autorización son cosas diferentes Supongamos que recibes: pedido_id = 500 El ID existe. Entonces la validación es correcta. Pero todavía necesitas preguntar: 👉 ¿El usuario autenticado tiene permiso para ver ese pedido? Podemos tener: Pedido 500 existe ✅ Formato correcto ✅ Usuario autenticado ✅ pero: Pedido pertenece a otro usuario ❌ El backend debe rechazar la operación. Por eso validar datos y comprobar permisos son responsabilidades distintas. 🚀 Ejemplo de autorización Una petición: GET /pedidos/500 no debería hacer únicamente: Buscar pedido 500 ↓ Devolver Debería considerar: Buscar pedido 500 ↓ ¿Existe? ↓ ¿Pertenece al usuario? ↓ Sí → devolver No → rechazar No basta con validar que el ID sea correcto. 📱 ¿Y si los datos vienen de una app móvil? Exactamente la misma regla. Puedes controlar completamente el código de tu aplicación móvil. Pero una vez publicada, alguien puede analizar sus peticiones y reproducirlas desde otro cliente. El backend no debería asumir: "Si viene de mi app móvil, entonces es seguro." Desde el punto de vista del servidor, sigue siendo información externa. 🤝 ¿Y si vienen de otra API? Tampoco deberías confiar automáticamente. Supongamos que integras un sistema externo. Recibes: { "estado": "pagado" } Aunque confíes en ese proveedor, todavía conviene comprobar: Que la petición realmente venga del origen esperado. Que tenga la firma correcta. Que la estructura sea válida. Que el evento corresponda a una operación existente. Toda entrada externa necesita algún nivel de validación. 🧾 Validar la estructura de una petición Supongamos que esperas: { "nombre": "Herman", "email": "correo@email.com" } Pero recibes: { "nombre": [], "email": 123, "dato_extra": "algo" } El backend debería detectar que la estructura no cumple con el contrato esperado. Por eso frameworks y librerías suelen permitir definir esquemas. Conceptualmente: nombre: string requerido máximo 100 email: string formato email requerido Si no se cumple: 400 Bad Request 🧱 Rechazar campos inesperados En algunos sistemas puede ser conveniente rechazar propiedades que no estaban definidas. Por ejemplo, esperas: nombre email pero recibes: nombre email rol Puedes responder: Campo inesperado: rol Esto ayuda a detectar clientes mal implementados y reduce el riesgo de aceptar accidentalmente información sensible. ⚠️ Nunca construyas consultas SQL con entrada directa Validar la entrada también es importante para evitar ciertos ataques. Una mala práctica sería construir una consulta concatenando directamente valores enviados por el usuario. Conceptualmente: "SELECT * FROM usuarios WHERE email = '" + email + "'" Esto puede abrir la puerta a SQL Injection . En lugar de eso deben utilizarse: Consultas parametrizadas. Prepared statements. ORMs correctamente utilizados. Por ejemplo: SELECT * FROM usuarios WHERE email = ? La entrada del usuario se trata como un dato y no como parte del código SQL. 🧼 Validar y sanitizar no son exactamente lo mismo Estos conceptos suelen confundirse. Validar significa decidir si un valor cumple las reglas. Por ejemplo: edad = -5 ↓ Inválido Sanitizar significa transformar o limpiar determinados valores antes de utilizarlos en cierto contexto. Por ejemplo: Eliminar espacios innecesarios Normalizar texto Pero sanitizar no debería utilizarse como excusa para aceptar cualquier cosa. Si el dato no tiene sentido, muchas veces lo correcto es rechazarlo. 📝 Ejemplo con nombres El usuario envía: " Herman " El backend puede normalizar: "Herman" Eso puede ser razonable. Pero si recibe: "" no tiene sentido convertirlo mágicamente en otro valor. Debe rechazarse si el campo es obligatorio. 🔢 Conversión automática también puede ser peligrosa Algunos frameworks convierten automáticamente: "25" a: 25 Eso puede ser cómodo. Pero hay que saber exactamente qué comportamiento tendrá la aplicación. Por ejemplo: "abc" no debería terminar convirtiéndose silenciosamente en: 0 si ese resultado cambia la lógica del negocio. Las conversiones implícitas deben manejarse con cuidado. 🚀 La base de datos también puede validar La validación no termina necesariamente en el backend. La propia base de datos puede proteger ciertas reglas mediante: NOT NULL UNIQUE FOREIGN KEY CHECK Por ejemplo: CHECK (precio >= 0) puede impedir que exista: precio = -500 aunque un error en el código intente guardarlo. Esto crea múltiples capas de protección. 🧩 Defensa en profundidad Una arquitectura saludable puede tener: Frontend ↓ Validación UX Backend ↓ Validación de entrada ↓ Reglas de negocio ↓ Autorización Base de datos ↓ Restricciones Cada capa cumple un propósito. No se trata de confiar completamente en una sola. 🧠 ¿Por qué seguir validando en frontend entonces? Porque la experiencia del usuario importa. Sin validación frontend: Usuario completa formulario ↓ Enviar ↓ Esperar servidor ↓ Error: correo inválido Con validación frontend: Usuario escribe correo incorrecto ↓ Mensaje inmediato Eso es mucho más agradable. Entonces no debemos eliminar la validación frontend. Simplemente entender que: 👉 No es una barrera de seguridad suficiente. ⚠️ Botones deshabilitados tampoco protegen la API Supongamos que tienes: <button disabled> Eliminar cuenta </button> Visualmente el usuario no puede presionarlo. Pero si conoce el endpoint: DELETE /usuarios/15 podría intentar llamarlo directamente. El backend debe comprobar si esa acción realmente está permitida. La interfaz controla la experiencia. La API controla la operación. 🚨 Ocultar un botón tampoco es autorización Imagina que los administradores ven: [Eliminar usuario] y usuarios normales no. Eso está bien visualmente. Pero un usuario normal podría descubrir el endpoint. Por eso esta lógica: No mostrar botón debe complementarse con: Backend: ¿Es administrador? Si no: 403 Forbidden Ocultar elementos no reemplaza los permisos. 🔐 Datos especialmente sensibles Hay ciertos valores que casi nunca deberían confiarse al cliente. Por ejemplo: rol permisos saldo precio final descuento autorizado propietario del recurso estado interno El usuario podría enviar una intención. Pero el backend debe calcular o verificar el valor real. Por ejemplo: Cliente: cupón = VERANO20 El backend debe comprobar: ¿Existe? ¿Está activo? ¿Aplica a este usuario? ¿Aplica a este producto? ¿Cuánto descuento corresponde? No debería recibir: descuento = 90% y aplicarlo directamente. 💰 Ejemplo con total de compra Una mala implementación: { "productos": [25, 30], "total": 1 } Backend: total recibido = 1 ↓ Cobrar $1 Una mejor estrategia: productos = [25, 30] ↓ Consultar precios reales ↓ Calcular subtotal ↓ Aplicar descuentos válidos ↓ Calcular impuestos ↓ Calcular total ↓ Cobrar El servidor debe ser la fuente de verdad para la lógica crítica. 🧪 También debes probar entradas inválidas Muchos desarrolladores prueban únicamente: nombre correcto email correcto cantidad correcta Pero también deberías probar: campo vacío número negativo texto demasiado largo tipo incorrecto campo adicional ID inexistente usuario sin permisos Una API no solamente debe funcionar con entradas correctas. También debe fallar correctamente ante entradas incorrectas. 📏 Límites de tamaño Imagina un campo: nombre que normalmente tiene: 30 caracteres Pero alguien envía: 10 millones de caracteres Aunque técnicamente sea un string, no significa que debas aceptarlo. Por eso conviene definir límites. Por ejemplo: nombre: máximo 100 caracteres Lo mismo ocurre con: Archivos. Arrays. JSON. Comentarios. Imágenes. Los límites también forman parte de la validación. 📁 Subida de archivos Si permites cargar archivos, tampoco deberías confiar únicamente en: accept=".jpg" del frontend. Un usuario puede saltarse esa restricción. El backend debería revisar aspectos como: Tamaño Tipo permitido Extensión Formato real y almacenarlos de forma segura. La validación del navegador es solo una comodidad. 🧠 Los errores también deben diseñarse Cuando un dato es inválido, la API debería responder de manera clara. Por ejemplo: { "error": "El campo cantidad debe ser mayor a 0" } Esto permite que el frontend muestre un mensaje útil. Pero hay que evitar respuestas que revelen información sensible. Por ejemplo, no deberíamos devolver detalles internos como: Ruta interna del servidor Credenciales Stack traces completos Consultas SQL especialmente en producción. 📋 Un formato consistente ayuda Puedes definir una estructura uniforme para errores: { "error": "validation_error", "fields": { "email": "Formato inválido", "edad": "Debe ser mayor a 18" } } Esto facilita que frontend y backend trabajen juntos. El cliente sabe exactamente cómo interpretar la respuesta. ⚠️ Validar absolutamente todo tampoco significa llenar el código de if Una mala implementación puede terminar así: if nombre... if email... if edad... if precio... if cantidad... repetido por todos lados. Los frameworks modernos suelen ofrecer: Schemas. Serializers. DTOs. Validators. Form objects. Estas herramientas ayudan a centralizar las reglas. El objetivo no es escribir validaciones manuales desorganizadas. Es establecer contratos claros. 🧩 Ejemplo conceptual de esquema Podríamos definir: CrearUsuario nombre: string requerido máximo 100 email: string requerido formato email edad: integer mínimo 18 máximo 120 Entonces todas las peticiones que no cumplan ese esquema se rechazan antes de llegar a la lógica principal. Esto mantiene el código mucho más limpio. 🌐 La API es una frontera de confianza Una forma útil de pensarlo es: Exterior ↓ ──── frontera ──── ↓ Backend Todo lo que cruza esa frontera debe considerarse potencialmente incorrecto. No necesariamente malicioso. A veces simplemente puede ser: Un bug del frontend. Una versión antigua de la app. Una integración mal implementada. Un dato corrupto. Un error humano. La validación también protege contra errores accidentales. 🚀 No todo usuario intenta atacar tu sistema Este punto también es importante. Cuando decimos: "No confíes en el cliente" no significa asumir que cada usuario es un atacante. Significa diseñar el sistema de forma que su integridad no dependa de que todos los clientes se comporten perfectamente. Incluso tu propio frontend puede tener bugs. El backend debe mantener sus reglas independientemente de eso. 🛠️ Buenas prácticas ✔️ Valida tipos, formatos, longitudes y rangos en el backend. ✔️ Define claramente qué campos acepta cada endpoint. ✔️ Rechaza o ignora campos inesperados según tu diseño. ✔️ No confíes en precios, roles, permisos o totales enviados por el cliente. ✔️ Calcula valores sensibles desde fuentes controladas por el servidor. ✔️ Comprueba autorización además de validación. ✔️ Utiliza consultas parametrizadas u ORMs correctamente configurados. ✔️ Mantén restricciones importantes también en la base de datos. ✔️ Define límites para textos, arrays y archivos. ✔️ Devuelve errores claros sin exponer información interna. 🧩 La realidad La interfaz que construyes no es una barrera de seguridad. Un formulario puede decir: Solo números positivos pero alguien puede enviar: -500 Un dropdown puede permitir únicamente: usuario moderador pero alguien puede intentar enviar: admin Un botón puede estar deshabilitado. Pero la API todavía puede recibir la petición. Por eso toda información externa debe pasar por validaciones y reglas definidas por el servidor. 💬 La validación del frontend mejora la experiencia. La validación del backend protege la integridad del sistema. Y las restricciones de la base de datos pueden funcionar como una última capa de defensa. 👉 La próxima vez que agregues una validación a un formulario, pregúntate también: ¿Qué ocurriría si alguien enviara esta petición sin utilizar mi formulario? Si tu backend continúa protegiendo correctamente la operación, entonces vas por buen camino. 🔥 El backend no se ve, pero sin él, nada funciona.

23 ago 2026
Leer publicación
Una petición falla… ¿deberías intentarlo otra vez?
Arquitectura de Software

Una petición falla… ¿deberías intentarlo otra vez?

Tu backend necesita comunicarse con otro servicio. Envía una petición. Pero algo ocurre. ⏱️ El servidor tarda demasiado. 🌐 La conexión falla temporalmente. ⚠️ El servicio responde con un error. En ese momento aparece una decisión importante: 👉 ¿Debemos volver a intentarlo? Muchas veces sí. Pero hacerlo sin control puede convertir un pequeño fallo temporal en un problema mucho mayor. Para eso existen las políticas de reintentos . 🧠 ¿Qué es una política de reintentos? Una política de reintentos, o Retry Policy , define qué debe hacer una aplicación cuando una operación falla. En lugar de abandonar inmediatamente, el sistema puede esperar un momento y volver a intentarlo. Por ejemplo: Petición ↓ ❌ Falla ↓ Esperar ↓ Reintento 1 ↓ ❌ Falla ↓ Esperar más ↓ Reintento 2 ↓ ✅ Correcto La idea es sencilla: si el error fue temporal, un segundo intento puede resolverlo sin que el usuario siquiera lo note. 🌐 ¿Por qué una petición puede fallar temporalmente? En sistemas distribuidos, que una solicitud falle no significa necesariamente que exista un problema permanente. Puede ocurrir por razones como: Una pequeña interrupción de red. Un servidor saturado. Un timeout. Un balanceador momentáneamente ocupado. Un servicio reiniciándose. Una dependencia externa con problemas temporales. Por ejemplo: Backend ↓ Servicio externo ↓ 503 Service Unavailable Tal vez el servicio vuelva a estar disponible 500 milisegundos después. En ese caso, reintentar puede ser una estrategia válida. ⚙️ Pero no todos los errores deberían reintentarse Este punto es fundamental. Supongamos que envías: POST /usuarios con un correo inválido. El servidor responde: 400 Bad Request Si vuelves a enviar exactamente la misma información: Intento 1 → 400 Intento 2 → 400 Intento 3 → 400 nada va a cambiar. El problema no es temporal. La petición está mal construida. Reintentar solamente genera tráfico innecesario. 🚦 ¿Qué errores suelen ser candidatos? Depende del contexto, pero normalmente tiene más sentido considerar errores como: Timeout Connection reset 502 Bad Gateway 503 Service Unavailable 504 Gateway Timeout porque pueden representar fallos temporales. En cambio, errores como: 400 Bad Request 401 Unauthorized 403 Forbidden 404 Not Found generalmente no se solucionan simplemente esperando y repitiendo exactamente la misma petición. Siempre debe evaluarse el significado real del error. 🚀 Un ejemplo real con pagos Imagina que tu aplicación necesita conectarse con una pasarela de pagos. Envía: Procesar pago Pero el proveedor responde: 503 Service Unavailable Tu backend puede decidir: Esperar 500 ms ↓ Intentar nuevamente Si el segundo intento funciona: 200 OK el fallo queda resuelto automáticamente. El usuario quizá nunca se entere de que existió un problema. Ese es uno de los principales beneficios de una buena política de reintentos. ⚠️ Pero aquí aparece un problema importante Imagina que el proveedor sí procesó el pago. Pero tu backend nunca recibió la respuesta. El flujo fue: Backend ↓ Proveedor ↓ Pago procesado ↓ ❌ Respuesta perdida Desde tu aplicación parece que la operación falló. Entonces decides reintentar. Si la operación no está protegida, podrías procesar el pago nuevamente. Por eso los reintentos deben combinarse con otro concepto importante: 👉 idempotencia . 🔐 Reintentos e idempotencia Si una operación crítica puede ejecutarse más de una vez, deberías diseñarla para reconocer duplicados. Por ejemplo: Idempotency-Key: pago-ABC123 Primer intento: pago-ABC123 ↓ Procesar ↓ Pago realizado Segundo intento: pago-ABC123 ↓ Ya procesado ↓ No volver a cobrar Así puedes reintentar de forma mucho más segura. 🔁 ¿Por qué no reintentar inmediatamente? Supongamos que un servicio está teniendo problemas. Tu aplicación envía una solicitud. Falla. Entonces haces esto: Retry Retry Retry Retry Retry sin esperar. Eso puede empeorar la situación. El servicio ya está saturado. Y ahora le estás enviando todavía más tráfico. 💥 El problema de una retry storm Imagina que tienes: 10,000 solicitudes por segundo y todas comienzan a fallar. Si cada una realiza tres reintentos inmediatos, podrías generar: 10,000 solicitudes originales + 30,000 reintentos = 40,000 solicitudes Justo cuando el servicio está teniendo más dificultades. Esto puede provocar una retry storm . En lugar de ayudar al sistema a recuperarse, terminas aumentando todavía más la carga. ⏳ Aquí entra el backoff Una estrategia mejor consiste en esperar antes de cada reintento. A esto se le conoce como: Backoff Por ejemplo: Intento 1 ↓ Falla ↓ Esperar 1 segundo Intento 2 ↓ Falla ↓ Esperar 2 segundos Intento 3 ↓ Falla ↓ Esperar 4 segundos Cada vez esperamos un poco más. Esto da tiempo al servicio para recuperarse. 📈 Backoff exponencial Una estrategia muy común es el Exponential Backoff . El tiempo de espera aumenta progresivamente. Por ejemplo: 1 segundo 2 segundos 4 segundos 8 segundos 16 segundos En lugar de insistir constantemente: 1s 1s 1s 1s 1s el sistema reduce gradualmente la presión sobre la dependencia. 🎲 ¿Qué es jitter? Ahora imagina que tienes 10,000 clientes. Todos fallan exactamente al mismo tiempo. Todos utilizan backoff: Esperar 5 segundos Cinco segundos después... los 10,000 vuelven a intentarlo exactamente al mismo tiempo. Eso puede generar otro pico. Para evitarlo se puede utilizar jitter . ⚙️ ¿Cómo funciona el jitter? En lugar de esperar exactamente: 5 segundos cada cliente espera una pequeña variación. Por ejemplo: Cliente A → 4.8 segundos Cliente B → 5.3 segundos Cliente C → 4.5 segundos Cliente D → 5.7 segundos Así los reintentos quedan distribuidos a lo largo del tiempo. Esto reduce el riesgo de provocar otro pico de tráfico sincronizado. ⏱️ Los timeouts son igual de importantes Una política de reintentos necesita timeouts. Supongamos que haces: Petición ↓ Esperar indefinidamente El sistema podría quedarse bloqueado durante demasiado tiempo. En cambio, puedes definir: Timeout = 3 segundos Si el servicio no responde: 3 segundos ↓ Cancelar ↓ Evaluar retry Esto evita que recursos importantes permanezcan ocupados sin límite. ⚠️ Timeout demasiado largo Imagina: Timeout = 60 segundos y después: 3 reintentos En el peor escenario, una operación podría tardar varios minutos antes de fallar definitivamente. Eso puede ser inaceptable para una API. Por eso timeout y cantidad de reintentos deben diseñarse juntos. ⚠️ Timeout demasiado corto El extremo contrario también puede ser problemático. Si configuras: Timeout = 100 ms pero el servicio normalmente responde en: 150 ms vas a generar errores artificialmente. Y después reintentos innecesarios. Por eso los valores deberían basarse en métricas reales. 🧠 Una política de retry tiene varias decisiones No consiste simplemente en: retry = true Debes definir: ¿Qué errores reintentar? ¿Cuántas veces? ¿Cuánto esperar? ¿Qué timeout usar? ¿Usar backoff? ¿Usar jitter? ¿Qué hacer al agotar intentos? Estas decisiones dependen del servicio y del impacto que tenga la operación. 🚀 Ejemplo de una política Podríamos tener: Timeout: 3 segundos Máximo de reintentos: 3 Backoff: Exponencial Jitter: Sí Errores reintentables: 502 503 504 Timeout El comportamiento sería: Request ↓ 503 ↓ Esperar ~1s ↓ Retry ↓ 503 ↓ Esperar ~2s ↓ Retry ↓ 200 La solicitud terminó funcionando sin insistir agresivamente. 🧩 ¿Qué ocurre cuando se agotan los reintentos? En algún momento tienes que dejar de intentarlo. Por ejemplo: Intento 1 ❌ Intento 2 ❌ Intento 3 ❌ Después: Stop A partir de ahí la aplicación debe decidir qué hacer. Podría: Devolver un error. Guardar una tarea para después. Enviar el trabajo a una cola. Utilizar un fallback. Alertar al equipo. Depende completamente del tipo de operación. 📧 Un ejemplo donde puede esperarse Supongamos que intentas enviar un correo de bienvenida. El proveedor falla. Puedes hacer algunos reintentos. Si todos fallan, quizá no sea necesario bloquear el registro del usuario. Podrías guardar: Correo pendiente y procesarlo posteriormente mediante una cola. El usuario ya quedó registrado. El correo puede enviarse después. 💳 Un ejemplo donde necesitas más cuidado En pagos la situación es distinta. No puedes simplemente asumir: Falló ↓ Intentar indefinidamente Porque el primer intento podría haberse procesado aunque tú no hayas recibido la respuesta. Aquí necesitas: Idempotencia. Estados claros. Confirmaciones. Reconciliación. Los reintentos por sí solos no solucionan el problema. 🔥 Reintentos + Circuit Breaker Las políticas de retry suelen combinarse con el patrón Circuit Breaker . Por ejemplo: Request ↓ Retry controlado ↓ Servicio Si existen algunos fallos temporales: Retry puede resolverlos. Pero si el servicio lleva mucho tiempo fallando: Circuit Breaker OPEN entonces dejamos de enviar solicitudes temporalmente. ⚡ ¿Por qué combinarlos? Sin Circuit Breaker: Servicio caído ↓ Request ↓ 3 retries ↓ Request ↓ 3 retries ↓ Request ↓ 3 retries El sistema continúa enviando tráfico. Con Circuit Breaker: Muchos errores ↓ Circuit OPEN ↓ No más solicitudes temporalmente Así se evita insistir sobre una dependencia que claramente está teniendo problemas. 🧱 Reintentos en varios niveles pueden multiplicarse Este es un problema menos evidente. Imagina: Frontend ↓ Backend A ↓ Backend B ↓ Servicio externo Cada nivel tiene: 3 reintentos Una sola acción del usuario podría generar muchas más llamadas de las esperadas. Por ejemplo, si cada capa reintenta la anterior: 3 × 3 × 3 podrías terminar con una gran cantidad de intentos. Por eso las políticas de reintentos deben coordinarse entre componentes. 🎯 No todos los servicios deberían reintentar En algunas arquitecturas es mejor decidir un único punto responsable de los reintentos. Por ejemplo: Cliente ↓ API ↓ Servicio externo Quizá únicamente la API implemente retry. Así tienes mejor control sobre: Cantidad total de intentos Latencia Carga Evitas que cada capa haga lo mismo sin saber qué están haciendo las demás. 📡 También debes respetar al servicio externo Algunas APIs responden con encabezados indicando cuánto tiempo deberías esperar. Por ejemplo: Retry-After: 30 Esto puede significar: "Espera 30 segundos antes de volver a intentarlo." Ignorar esa información y bombardear el servicio puede empeorar el problema o provocar bloqueos. Cuando un proveedor ofrece instrucciones de retry, conviene respetarlas. 🚦 429 Too Many Requests Un caso importante es: 429 Too Many Requests Significa que estás enviando más solicitudes de las permitidas. Reintentar inmediatamente sería exactamente lo contrario de lo que necesitas. Lo correcto puede ser: 429 ↓ Esperar ↓ Reducir ritmo ↓ Reintentar después De nuevo, el backoff es importante. 🔍 Monitorear los reintentos Si tu sistema está haciendo muchos retries, probablemente algo esté ocurriendo. Puedes medir: Requests totales Retries realizados Retries exitosos Retries agotados Por ejemplo: 1,000 requests 200 retries Eso podría ser una señal de que alguna dependencia está degradada. 📊 Una métrica interesante: retry rate Podrías observar: Retry Rate = 2% en condiciones normales. Pero de repente: Retry Rate = 45% Eso indica que algo cambió. Aunque la mayoría de usuarios siga recibiendo respuestas correctas, tu sistema está teniendo que trabajar mucho más para conseguirlas. 🚨 Los reintentos pueden ocultar problemas Esto también es importante. Supongamos que un servicio falla una vez de cada tres solicitudes. Gracias al retry: Intento 1 ❌ Intento 2 ✅ los usuarios quizá no noten nada. Pero el problema sigue existiendo. Si no monitoreas los reintentos, podrías pensar: "Todo funciona perfectamente." cuando realmente existe una dependencia inestable. 🧪 Probar escenarios de fallo No deberías probar solamente: Servicio responde 200 También conviene probar: Timeout 503 429 Conexión interrumpida Servicio lento Tres errores consecutivos Y verificar: ¿Cuántos retries realiza? ¿Cuánto tarda? ¿Respeta el máximo? ¿Aplica backoff? ¿Genera duplicados? La resiliencia también necesita pruebas. ⚠️ Cuidado con las operaciones no idempotentes Reintentar: GET /products normalmente es relativamente seguro. Pero reintentar: POST /payments puede tener efectos secundarios. Por eso antes de aplicar una política automática hay que preguntarse: 👉 ¿Qué pasa si esta operación realmente sí se ejecutó la primera vez? Si la respuesta es: Se cobraría dos veces necesitas protección adicional. 🛠️ Buenas prácticas ✔️ Define un máximo de reintentos. ✔️ Utiliza timeout para no esperar indefinidamente. ✔️ Reintenta solamente errores que puedan ser temporales. ✔️ Utiliza backoff para espaciar intentos. ✔️ Agrega jitter cuando muchos clientes puedan fallar simultáneamente. ✔️ Protege operaciones críticas con idempotencia. ✔️ Respeta Retry-After cuando el servicio lo proporcione. ✔️ Evita políticas de retry duplicadas en muchas capas. ✔️ Combina retry con Circuit Breaker cuando exista riesgo de fallas prolongadas. ✔️ Monitorea cuántos reintentos realiza realmente tu aplicación. 🧩 La realidad Una petición puede fallar por razones completamente temporales. En esos casos, volver a intentarlo puede hacer que el problema desaparezca sin afectar al usuario. Pero un reintento mal diseñado puede transformar: Un pequeño error en: Miles de nuevas solicitudes ↓ Más carga ↓ Más errores ↓ Más reintentos ↓ Servicio completamente saturado La diferencia está en tener una política clara. No se trata de intentar hasta que funcione. Se trata de saber: Cuándo intentar Cuánto esperar Cuántas veces hacerlo Cuándo detenerse 💬 La resiliencia no consiste en insistir infinitamente. Consiste en reconocer cuándo un fallo probablemente sea temporal, recuperarse de él de forma controlada y saber cuándo dejar de intentarlo. 👉 La próxima vez que implementes un retry , no pienses solamente en el segundo intento. Piensa también en qué ocurriría si miles de solicitudes estuvieran haciendo exactamente lo mismo al mismo tiempo. 🔥 El backend no se ve, pero sin él, nada funciona.

22 ago 2026
Leer publicación
🔗 ¿Qué es una clave foránea y cómo conecta las tablas de una base de datos?
Base de datos

🔗 ¿Qué es una clave foránea y cómo conecta las tablas de una base de datos?

Imagina que estás desarrollando una tienda en línea. Tienes una tabla con todos los usuarios. Otra con los pedidos. Otra con los productos. Otra con las direcciones. Otra con los pagos. Ahora imagina que un usuario realiza una compra. Surge una pregunta muy importante. 👉 ¿Cómo sabe la base de datos que ese pedido pertenece exactamente a ese usuario? ¿Dónde guarda esa relación? ¿Tiene que copiar toda la información del cliente dentro de cada pedido? La respuesta es: No. Para eso existen las claves foráneas (Foreign Keys). Son uno de los pilares de cualquier base de datos relacional y permiten que distintas tablas trabajen juntas como si fueran una sola. 🧠 ¿Qué es una clave foránea? Una clave foránea ( Foreign Key ) es un campo que crea una relación entre dos tablas. Su función consiste en apuntar hacia la clave primaria de otra tabla. En otras palabras... es un enlace. Una referencia. Un puente que conecta información relacionada. Gracias a ella la base de datos entiende cómo se relacionan los datos sin necesidad de duplicarlos. 💡 Piensa en una credencial escolar Imagina una universidad. Existe un expediente para cada alumno. Cada uno tiene un número único. Por ejemplo: Matrícula: 202500123 Ahora imagina la biblioteca. Cuando un estudiante pide un libro... la biblioteca no escribe nuevamente: 👤 Nombre completo. 🏠 Dirección. 📞 Teléfono. Solo registra la matrícula. Porque esa matrícula ya identifica al alumno. La clave foránea funciona exactamente igual. ⚙️ Un ejemplo sencillo Supongamos estas dos tablas. 👤 Usuarios ID Nombre 1 Ana 2 Carlos 📦 Pedidos Pedido Usuario_ID 101 1 102 2 103 1 Observa la columna: Usuario_ID Ese campo es una clave foránea . No guarda el nombre del usuario. Guarda el identificador del usuario. Gracias a eso la base de datos sabe inmediatamente que: 📦 Pedido 101 pertenece a Ana. 📦 Pedido 102 pertenece a Carlos. 📦 Pedido 103 también pertenece a Ana. Y todo sin repetir el nombre del cliente una y otra vez. 🚀 ¿Por qué es tan importante? Las claves foráneas permiten: ✅ Relacionar información entre tablas. ✅ Evitar duplicar datos. ✅ Mantener la consistencia. ✅ Facilitar consultas complejas. ✅ Reducir errores. Sin ellas... cada tabla sería completamente independiente. Y relacionar información sería mucho más complicado. 💡 Imagina una tienda enorme Supongamos que tienes: 👤 2 millones de usuarios. 📦 50 millones de pedidos. Si cada pedido guardara: Nombre. Correo. Dirección. Teléfono. Toda esa información estaría repetida millones de veces. Eso significaría: 📈 Más espacio ocupado. 🐌 Consultas más lentas. ⚠️ Mayor riesgo de inconsistencias. Con una clave foránea... cada pedido solo guarda algo como: Usuario_ID = 25 Y cuando necesita conocer el nombre del cliente... simplemente consulta la tabla Usuarios. Mucho más eficiente. 🌐 ¿Qué sucede cuando consultas información? Imagina esta consulta. Quieres obtener todos los pedidos de Ana. La base de datos hace algo parecido a esto. Primero encuentra: Ana Su ID es: 1 Después busca todos los pedidos donde: Usuario_ID = 1 Y automáticamente obtiene: 📦 Pedido 101. 📦 Pedido 103. Todo gracias a la relación creada por la clave foránea. 💳 Un ejemplo bancario Supongamos una aplicación bancaria. Existe una tabla llamada: 🏦 Cuentas. Y otra llamada: 💸 Transferencias. Cada transferencia pertenece a una cuenta. No tendría sentido guardar nuevamente: Nombre del cliente. CURP. Dirección. Correo. Cada transferencia solo necesita almacenar: Cuenta_ID Después, cuando sea necesario... la base de datos recupera toda la información del titular desde la tabla correspondiente. 🛒 Otro ejemplo muy común Piensa en Amazon. Un pedido puede contener varios productos. Existe una tabla: 📦 Productos. Y otra: 🛒 Detalle_Pedido. Cada fila del detalle contiene algo parecido a esto. Pedido_ID Producto_ID Cantidad Observa que: Producto_ID es otra clave foránea. Gracias a ella la base de datos sabe exactamente qué producto forma parte del pedido. 🌐 ¿Qué sucede si intentas guardar información inválida? Supongamos que intentas registrar este pedido. Pedido: 104 Usuario_ID: 99 Pero en la tabla Usuarios no existe ningún usuario con ID 99. Si la clave foránea está correctamente configurada... la base de datos responde algo parecido a: ❌ Operación rechazada. ¿Por qué? Porque impedir relaciones inválidas es precisamente una de sus funciones. Así evita que aparezcan pedidos de usuarios inexistentes. 🛡️ Esto se conoce como integridad referencial Las claves foráneas ayudan a mantener algo llamado: Integridad referencial. Suena complicado. Pero la idea es muy sencilla. 👉 Toda referencia debe apuntar a un registro que realmente exista. Eso garantiza que la información permanezca consistente. Y evita errores muy difíciles de detectar. ⚠️ ¿Qué ocurre si eliminas un usuario? Aquí aparece otro escenario interesante. Supongamos que existe: 👤 Usuario 25. Y tiene: 📦 350 pedidos. ¿Qué debería hacer la base de datos si eliminas ese usuario? Existen varias estrategias. 🚫 RESTRICT No permite eliminar el usuario. Porque todavía existen pedidos relacionados. 🗑️ CASCADE Elimina también todos los pedidos relacionados. 🔄 SET NULL Mantiene los pedidos. Pero elimina la relación. Cada proyecto decide cuál estrategia tiene más sentido. ⚠️ Un error muy común Muchos principiantes hacen algo parecido a esto. Tabla Pedidos. Pedido NombreCliente CorreoCliente TelefonoCliente DireccionCliente Todo queda duplicado. Si el cliente cambia su nombre. Hay que modificar cientos de pedidos. Si cambia su correo. Hay que actualizar miles de registros. Eso rompe uno de los principios más importantes del diseño de bases de datos. Con una clave foránea... solo existe un registro del usuario. Todos los pedidos apuntan hacia él. 🛠️ ¿Qué tipos de relaciones permiten? Las claves foráneas hacen posible prácticamente todas las relaciones de una base de datos. 👤 Uno a Uno (1:1) Un usuario. Una configuración de perfil. 📦 Uno a Muchos (1:N) Un usuario. Muchos pedidos. 🔄 Muchos a Muchos (N:M) Un estudiante. Muchos cursos. Y cada curso tiene muchos estudiantes. Aquí normalmente aparece una tabla intermedia que contiene dos claves foráneas. 🌐 ¿Dónde las utilizas todos los días? Aunque no lo notes... las claves foráneas están presentes en prácticamente cualquier aplicación. 📦 Pedidos y clientes. 💬 Comentarios y publicaciones. ❤️ Likes y usuarios. 📧 Correos y destinatarios. 🎵 Canciones y álbumes. 🎬 Películas y actores. 📚 Libros y autores. Son el mecanismo que mantiene unidas todas esas piezas. ⚠️ Otro error frecuente Pensar que una clave foránea sirve únicamente para relacionar tablas. También protege la información. Porque impide crear relaciones inexistentes. Eso convierte a la propia base de datos en una capa adicional de validación. Incluso aunque exista un error en el backend. 🧩 La realidad Cada vez que consultas los pedidos de un cliente... los comentarios de una publicación... las materias inscritas por un alumno... o los productos de una compra... la base de datos está utilizando claves foráneas para conectar información distribuida en distintas tablas. Y aunque nunca aparezcan en la interfaz... son una de las razones por las que una base de datos relacional puede organizar millones de registros sin perder consistencia. 🚀 Conclusión Las claves foráneas son el mecanismo que permite conectar tablas dentro de una base de datos relacional. Gracias a ellas es posible evitar información duplicada, mantener la integridad de los datos y construir relaciones claras entre diferentes entidades. Mientras la clave primaria identifica de forma única un registro... la clave foránea indica cómo ese registro se relaciona con otros. Sin ellas, las tablas serían simples listas independientes. Con ellas, se convierten en un sistema capaz de representar relaciones complejas de forma ordenada, eficiente y segura. 💬 Las claves primarias identifican los datos. Las claves foráneas construyen las relaciones entre ellos. 👉 ¿Cuál ha sido la relación más compleja que te ha tocado modelar en una base de datos: 1:1, 1:N o N:M? 👀 🔥 El backend no se ve, pero sin él, nada funciona. 📚 Versión completa y ampliada disponible en mi blog: https://hermanprimo.dev/blog/que-es-una-clave-foranea-y-como-conecta-las-tablas #SQL #BaseDeDatos #ForeignKey #Backend #SoftwareEngineering #DatabaseDesign #PostgreSQL #MySQL #ArquitecturaSoftware

21 ago 2026
Leer publicación
¿Por qué algunas páginas cargan rápido aunque tengan cientos de archivos y mucho contenido?
Rendimiento

¿Por qué algunas páginas cargan rápido aunque tengan cientos de archivos y mucho contenido?

Cuando visitas una página web, tu navegador necesita descargar información desde uno o varios servidores. Por ejemplo: 📄 HTML. 🎨 CSS. ⚙️ JavaScript. 📦 JSON. 🔤 Fuentes. 🧾 XML. Además de imágenes, videos y otros recursos. Mientras más información tenga que viajar por Internet, más tiempo puede tardar la página en estar lista. Y aunque tengas una conexión rápida, enviar menos datos sigue siendo una ventaja. Por eso existe una técnica muy utilizada: 👉 la compresión Gzip . 🧠 ¿Qué es Gzip? Gzip es un método de compresión que permite reducir el tamaño de ciertos archivos antes de enviarlos desde el servidor hacia el navegador. La idea es sencilla. Supongamos que un archivo JavaScript pesa: 500 KB En lugar de enviar los 500 KB completos, el servidor puede comprimirlo. Por ejemplo: 500 KB ↓ Gzip ↓ 120 KB El navegador recibe esos 120 KB, los descomprime automáticamente y luego procesa el archivo normalmente. Para el usuario el resultado es el mismo. Lo que cambió fue la cantidad de información transferida. ⚙️ ¿Cómo sabe el servidor que puede usar Gzip? Cuando el navegador hace una petición HTTP, puede indicar qué métodos de compresión soporta. Por ejemplo, puede enviar un encabezado parecido a: Accept-Encoding: gzip, br Eso significa: "Puedo recibir contenido comprimido con Gzip o Brotli." El servidor revisa esa información. Si tiene Gzip habilitado, puede comprimir la respuesta y devolver algo parecido a: Content-Encoding: gzip El navegador ve ese encabezado y sabe que debe descomprimir el contenido antes de utilizarlo. Todo ocurre automáticamente. 🔄 El flujo completo Podemos imaginarlo así: Navegador ↓ GET /app.js Accept-Encoding: gzip ↓ Servidor ↓ Comprime app.js ↓ Content-Encoding: gzip ↓ Navegador ↓ Descomprime ↓ Ejecuta JavaScript El desarrollador no necesita escribir código en el frontend para descomprimir manualmente el archivo. El navegador ya sabe hacerlo. 🚀 ¿Por qué reduce tanto algunos archivos? Gzip funciona especialmente bien cuando encuentra patrones repetidos. Y los archivos de texto suelen tener muchísima repetición. Por ejemplo, imagina un JSON: { "nombre": "Producto 1", "precio": 500, "disponible": true } Después: { "nombre": "Producto 2", "precio": 700, "disponible": true } Palabras como: nombre precio disponible se repiten constantemente. La compresión puede representar esas repeticiones de una manera más eficiente. Por eso formatos como: HTML. CSS. JavaScript. JSON. XML. SVG. pueden reducir bastante su tamaño. 📦 Un ejemplo con una API Supongamos que una API devuelve un catálogo grande. Sin compresión: Respuesta JSON: 2 MB Con Gzip podría reducirse, dependiendo del contenido, a algo como: 400 KB Entonces el navegador ya no necesita descargar 2 MB. Solamente 400 KB. Eso significa: Menos transferencia ↓ Menor tiempo de descarga ↓ Menor consumo de ancho de banda Especialmente en conexiones móviles o redes lentas, la diferencia puede ser importante. 📱 Esto importa todavía más en conexiones lentas Imagina dos usuarios. Usuario A tiene: 500 Mbps Usuario B tiene: 5 Mbps Enviar 2 MB puede parecer prácticamente instantáneo para el primero. Pero para el segundo puede tardar mucho más. Si comprimimos esos 2 MB a: 400 KB la mejora se vuelve mucho más evidente. Por eso la optimización del tamaño de las respuestas sigue siendo importante incluso si el servidor es muy rápido. 🌎 También importa la distancia La información no aparece mágicamente en el dispositivo. Debe viajar por una red. Dependiendo de dónde esté el usuario y dónde esté el servidor, los datos pueden atravesar: Navegador ↓ Router ↓ Proveedor de Internet ↓ Redes intermedias ↓ Centro de datos ↓ Servidor Reducir la cantidad de bytes que deben recorrer ese camino puede mejorar el tiempo total de carga. 🚀 Un ejemplo real con JavaScript Supongamos que una aplicación utiliza un bundle de JavaScript de: 1 MB Sin compresión: Servidor ↓ 1 MB ↓ Navegador Con Gzip: Servidor ↓ 250 KB ↓ Navegador La cifra exacta depende completamente del archivo. Pero en contenido textual las reducciones pueden ser considerables. Después el navegador descomprime esos 250 KB y obtiene nuevamente el JavaScript original. 🧠 Importante: el archivo sigue siendo de 1 MB después La compresión reduce el tamaño durante la transferencia. No significa necesariamente que el navegador termine procesando solamente 250 KB de JavaScript. Después de descomprimir: 250 KB comprimidos ↓ Descompresión ↓ 1 MB original El navegador todavía necesita: Parsear el JavaScript. Compilarlo. Ejecutarlo. Por eso Gzip mejora principalmente el costo de transferencia , no todos los costos relacionados con el archivo. ⚠️ Gzip no hace que JavaScript se ejecute más rápido Este es un error común. Supongamos que tienes un archivo JavaScript enorme y muy pesado. Gzip puede hacer que llegue más rápido. Pero después el navegador todavía tiene que ejecutar ese código. Entonces podemos tener: Transferencia rápida ✅ Ejecución pesada ❌ Gzip no soluciona: Código JavaScript ineficiente. Bloqueos del hilo principal. Demasiados scripts. Cálculos costosos. Renderizado lento. Son problemas diferentes. 🖼️ ¿Y las imágenes? Aquí hay que tener cuidado. Formatos como: JPEG PNG WebP AVIF ya utilizan sus propios métodos de compresión. Intentar aplicar Gzip encima normalmente ofrece muy poco beneficio. Incluso puede aumentar trabajo innecesariamente en el servidor. Por eso Gzip se utiliza principalmente para contenido textual. 🎥 Lo mismo ocurre con videos Archivos como: MP4 WebM ya están comprimidos utilizando códecs específicos. Comprimirlos nuevamente con Gzip normalmente no aporta demasiado. Sería como intentar comprimir otra vez un archivo que ya fue optimizado. 📦 ¿Qué archivos suelen beneficiarse? Normalmente: HTML ✅ CSS ✅ JavaScript ✅ JSON ✅ XML ✅ SVG ✅ Texto ✅ Mientras que: JPEG ⚠️ PNG ⚠️ MP4 ⚠️ ZIP ⚠️ PDF ⚠️ pueden ofrecer poco beneficio adicional dependiendo del contenido. La configuración debería considerar el tipo MIME de la respuesta. 🧩 ¿Dónde se configura Gzip? Puede habilitarse en distintos lugares de la infraestructura. Por ejemplo: Aplicación ↓ Servidor web ↓ Proxy inverso ↓ CDN La compresión puede realizarla alguno de esos componentes. Es muy común configurarla en servidores o proxies como: Nginx. Apache. Caddy. También muchas plataformas cloud y CDNs la manejan automáticamente. 🚀 Ejemplo con Nginx Una configuración puede incluir algo conceptualmente parecido a: gzip on; y definir qué tipos de contenido deberían comprimirse. Por ejemplo: gzip_types text/css application/javascript application/json; Así Nginx puede comprimir las respuestas antes de enviarlas al cliente. La configuración real debe ajustarse según la aplicación y el entorno. 🔥 ¿Comprimir siempre es gratis? No. Para reducir un archivo, el servidor necesita ejecutar un proceso de compresión. Eso consume: CPU Y después el navegador necesita descomprimirlo. También consume recursos. Normalmente el ahorro de transferencia compensa ampliamente ese costo para archivos adecuados. Pero existe un equilibrio. ⚖️ Compresión vs CPU Imagina un archivo de: 100 bytes Comprimirlo quizá ni siquiera valga la pena. El costo de realizar la operación puede ser mayor que el ahorro obtenido. Por eso muchos servidores permiten definir un tamaño mínimo. Por ejemplo: No comprimir respuestas menores a 1 KB La cifra correcta depende de la infraestructura. 🎚️ También existen niveles de compresión Gzip permite diferentes niveles. Podemos pensar conceptualmente en: Nivel bajo ↓ Menos CPU ↓ Archivo un poco más grande frente a: Nivel alto ↓ Más CPU ↓ Archivo un poco más pequeño Utilizar siempre la máxima compresión no necesariamente es la mejor decisión. Podrías gastar mucho más CPU para ahorrar solamente unos cuantos kilobytes adicionales. 🏗️ Archivos estáticos pueden comprimirse previamente Hay otra estrategia. En lugar de comprimir el mismo archivo cada vez que alguien lo solicita: Request 1 → comprimir Request 2 → comprimir Request 3 → comprimir puedes generar una versión comprimida durante el proceso de build o despliegue. Por ejemplo: app.js app.js.gz Entonces el servidor simplemente entrega la versión ya comprimida. Esto reduce el costo de CPU durante las solicitudes. 🌐 También existe Brotli Actualmente Gzip no es la única opción popular. También existe: Brotli Normalmente se identifica como: br Por ejemplo: Accept-Encoding: gzip, br Brotli puede ofrecer mejores niveles de compresión que Gzip en muchos tipos de contenido web. Por eso algunas plataformas modernas utilizan Brotli cuando el navegador lo soporta y Gzip como alternativa. 🔄 Negociación automática Una arquitectura puede hacer: ¿Cliente soporta Brotli? ├── Sí → enviar Brotli └── No ↓ ¿Soporta Gzip? ├── Sí → enviar Gzip └── No → enviar sin compresión De esta forma el servidor adapta la respuesta a las capacidades del cliente. 🧩 ¿Por qué Gzip sigue siendo tan utilizado? Porque tiene soporte extremadamente amplio. Navegadores, servidores web, proxies y librerías HTTP llevan muchos años soportándolo. Por eso continúa siendo una opción muy segura y compatible. Brotli puede ofrecer mejores resultados en ciertos escenarios, pero Gzip sigue siendo una tecnología fundamental de la web. 📦 ¿Qué ocurre con la caché? Cuando un recurso puede entregarse comprimido o sin comprimir, la infraestructura de caché debe saber que existen variantes. Por ejemplo: app.js → gzip app.js → brotli app.js → sin compresión Aquí puede aparecer el encabezado: Vary: Accept-Encoding Esto indica a caches y proxies que la respuesta puede cambiar dependiendo de lo que el cliente soporte. Es un detalle importante para evitar servir una versión incorrecta. 🔎 ¿Cómo saber si tu servidor realmente está comprimiendo? Puedes revisarlo desde las herramientas de desarrollo del navegador. En la pestaña Network , selecciona una petición. Puedes revisar encabezados como: Content-Encoding: gzip o: Content-Encoding: br Si aparece alguno de ellos, la respuesta está llegando comprimida. 📊 También puedes comparar tamaños Los navegadores suelen mostrar: Transferred y: Resource Size Por ejemplo: Transferred: 120 KB Resource Size: 500 KB Eso indica que viajaron menos datos por la red que el tamaño final del recurso descomprimido. Es una forma muy sencilla de comprobar el beneficio. 🚀 Gzip y minificación no son lo mismo Este punto también genera confusión. Supongamos este JavaScript: function calcularTotal(precio, cantidad) { return precio * cantidad; } Una herramienta de minificación podría convertirlo en algo parecido a: function a(b,c){return b*c} La minificación elimina caracteres innecesarios y reduce el archivo original. Gzip hace algo diferente. Comprime ese archivo durante la transferencia. 🧩 Se pueden utilizar juntos De hecho, normalmente tiene sentido hacer ambas cosas. JavaScript original ↓ Minificación ↓ Archivo más pequeño ↓ Gzip ↓ Transferencia todavía menor Por ejemplo: 1 MB original ↓ 700 KB minificado ↓ 180 KB comprimido Las cifras son solamente ilustrativas, pero muestran que ambas técnicas solucionan problemas diferentes. 🎨 Lo mismo aplica a CSS CSS puede contener: .card { background-color: white; padding: 20px; } La minificación puede eliminar espacios y saltos innecesarios. Después Gzip puede aprovechar patrones repetidos como: background padding color para reducir todavía más el tamaño durante la transferencia. 📄 HTML dinámico también puede comprimirse No solamente los archivos estáticos. Una respuesta generada dinámicamente: <html> ... </html> también puede comprimirse antes de enviarse. Lo mismo ocurre con una API que responde JSON. Por eso Gzip puede beneficiar tanto páginas tradicionales como aplicaciones modernas. 📡 APIs también se benefician mucho Imagina una API que responde: [ { "id": 1, "nombre": "Producto 1", "precio": 100 }, { "id": 2, "nombre": "Producto 2", "precio": 150 } ] Si devuelve miles de elementos, habrá muchas claves repetidas. Gzip puede reducir considerablemente ese contenido. En APIs que mueven grandes cantidades de JSON, la diferencia puede ser importante. ⚠️ Pero quizá el verdadero problema sea enviar demasiados datos Aquí hay otro punto importante. Supongamos que una API devuelve: 20 MB de JSON y Gzip lo reduce a: 4 MB Es una gran mejora. Pero quizá deberíamos preguntarnos: 👉 ¿El usuario realmente necesitaba esos 20 MB? Tal vez sería mejor utilizar: Paginación. Filtros. Campos seleccionados. Respuestas más pequeñas. La compresión no debería utilizarse como excusa para enviar información innecesaria. 🚀 Ejemplo con paginación En lugar de: GET /products ↓ 100,000 productos podemos usar: GET /products?page=1 ↓ 50 productos Y además aplicar compresión. Entonces combinamos: Menos información + Información comprimida Esto suele ser mucho mejor que depender únicamente de Gzip. 🖼️ Tampoco arregla imágenes enormes Supongamos que tu página tiene: HTML → 50 KB CSS → 100 KB JavaScript → 300 KB Imagen principal → 12 MB Gzip puede optimizar los primeros archivos. Pero la imagen de 12 MB probablemente seguirá siendo el gran problema. Ahí deberías utilizar: Compresión de imagen. WebP o AVIF. Tamaños adecuados. Responsive images. Lazy loading. Cada tipo de recurso necesita su propia estrategia. 🐢 Tampoco arregla un backend lento Imagina: Request ↓ Consulta SQL tarda 5 segundos ↓ Generar JSON ↓ Gzip ↓ Enviar en 100 ms La respuesta seguirá tardando más de cinco segundos. El problema principal está en la consulta. Gzip solo optimiza la última parte: Transferencia No debemos confundir velocidad de procesamiento con velocidad de transferencia. 🧠 Rendimiento web es la suma de muchas cosas Una página rápida puede depender de: Backend rápido + Consultas eficientes + Caché + CDN + Archivos pequeños + Compresión + Imágenes optimizadas + JavaScript eficiente Gzip es una pieza importante. Pero solamente una pieza. ⚠️ Otro error: comprimir información que ya viene comprimida Si intentamos aplicar Gzip a: archivo.zip estamos intentando comprimir un archivo que ya está comprimido. El ahorro normalmente será mínimo. Lo mismo puede ocurrir con: JPEG MP4 WebP y otros formatos. Esto consume CPU sin aportar demasiado. 🔐 La compresión también requiere una configuración consciente En ciertos contextos de seguridad, combinar compresión con secretos y contenido controlable por usuarios puede introducir riesgos específicos. Por eso no debería activarse de manera completamente indiscriminada para cualquier tipo de respuesta sensible. La configuración debe considerar qué contenido se comprime y cómo está construido el sistema. 📈 ¿Cuánto mejora realmente? No existe una cifra universal. Depende de: Tipo de archivo. Cantidad de texto repetido. Nivel de compresión. Tamaño original. Tecnología utilizada. Un archivo puede reducirse un: 20% mientras otro podría reducirse: 70% o incluso más. Por eso lo correcto es medir. 🛠️ Buenas prácticas ✔️ Habilita compresión para HTML, CSS, JavaScript, JSON, XML y otros formatos textuales. ✔️ Evita comprimir repetidamente archivos que ya utilizan compresión. ✔️ Considera Brotli además de Gzip cuando tu infraestructura lo soporte. ✔️ Define un tamaño mínimo para evitar trabajo innecesario. ✔️ Ajusta los niveles de compresión según el costo de CPU. ✔️ Considera precomprimir archivos estáticos. ✔️ Revisa Content-Encoding desde las herramientas del navegador. ✔️ Compara tamaño transferido contra tamaño real. ✔️ Combina Gzip con minificación. ✔️ No utilices compresión como sustituto de reducir datos innecesarios. 🧩 La realidad Una página rápida no depende únicamente de qué tan rápido puede procesar información el servidor. También importa cuántos datos necesita enviar. Podríamos tener: Servidor rápido + Respuesta de 10 MB y aun así ofrecer una experiencia lenta. En cambio: Respuesta optimizada + Compresión + Menos transferencia puede mejorar significativamente el tiempo de carga. Gzip aprovecha una idea muy sencilla: 👉 Si enviar datos por Internet cuesta tiempo, enviar menos datos puede ser más rápido. 💬 Optimizar una aplicación no siempre significa ejecutar código más rápido. A veces significa simplemente transformar: 500 KB en: 100 KB antes de enviarlos. 👉 La próxima vez que abras las herramientas del navegador, revisa Content-Encoding . Puede que gran parte de la velocidad de esa página venga de información que llegó comprimida sin que siquiera lo notaras. 🔥 El backend no se ve, pero sin él, nada funciona.

18 ago 2026
Leer publicación
¿Por qué algunos sistemas usan IDs como 1, 2, 3, 4... y otros utilizan identificadores enormes llenos de letras y números?
Backend

¿Por qué algunos sistemas usan IDs como 1, 2, 3, 4... y otros utilizan identificadores enormes llenos de letras y números?

Cuando guardas información en una base de datos, cada registro necesita una forma de identificarse. Durante muchos años, una de las estrategias más comunes ha sido utilizar números consecutivos: 1 2 3 4 5 Es sencillo. Es fácil de leer. Es rápido de generar. Y funciona perfectamente en muchísimos sistemas. Pero también es común encontrar identificadores como este: 550e8400-e29b-41d4-a716-446655440000 A primera vista parece mucho más complicado. 👉 Ese tipo de identificador es un UUID . Y aunque puede parecer excesivo para simplemente identificar un registro, resuelve problemas muy útiles en determinadas arquitecturas. 🧠 ¿Qué es un UUID? UUID significa: Universally Unique Identifier Es un identificador de 128 bits diseñado para ser prácticamente único. Normalmente se representa de esta forma: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Por ejemplo: 550e8400-e29b-41d4-a716-446655440000 La idea principal es que diferentes sistemas puedan generar identificadores sin tener que coordinarse entre ellos previamente. Eso es muy diferente a una secuencia tradicional. 🔢 ¿Cómo funciona un ID autoincremental? Imagina una tabla: usuarios ---------------- id nombre email Podemos configurar el campo id para aumentar automáticamente. Entonces: Herman → 1 Ana → 2 Carlos → 3 Laura → 4 Cuando llega un nuevo usuario, la base de datos genera: 5 Después: 6 Y así sucesivamente. Esto puede hacerse con mecanismos como: AUTO_INCREMENT IDENTITY SEQUENCE dependiendo del motor utilizado. ⚙️ ¿Qué ventaja tiene esto? Es extremadamente sencillo. Los IDs son pequeños. Ocupan poco espacio. Son fáciles de ordenar. Y suelen funcionar muy bien como claves primarias e índices. Por ejemplo: SELECT * FROM usuarios WHERE id = 25; La base de datos puede localizar rápidamente ese registro utilizando un índice. Para una enorme cantidad de aplicaciones, esto es más que suficiente. 🧩 Entonces, ¿por qué necesitamos UUID? Porque existen escenarios donde depender de una secuencia central resulta menos conveniente. Imagina que tenemos varios servidores creando información simultáneamente. Servidor A Servidor B Servidor C Todos necesitan generar nuevos pedidos. Si utilizamos una secuencia global: Pedido 100 Pedido 101 Pedido 102 algún componente necesita controlar cuál es el siguiente número disponible. Normalmente esa responsabilidad puede recaer en la base de datos. Eso funciona. Pero ahora todos dependen del mismo mecanismo central para generar identificadores. Con UUID, cada servidor puede crear uno por su cuenta. 🚀 Un ejemplo distribuido Supongamos que dos servidores reciben pedidos exactamente al mismo tiempo. Servidor A genera: 7b0f3c01-c804-4caa-a81c-8908b63165de Servidor B genera: ce75145e-a2bb-4eef-a3df-b29608409317 No necesitan preguntarse: "¿Qué ID vas a utilizar tú?" Tampoco necesitan solicitar primero: ¿Cuál es el siguiente número? Cada uno genera su identificador localmente. Después ambos registros pueden enviarse a la base de datos. 🌎 Esto es útil en sistemas distribuidos Imagina una aplicación ejecutándose en varias regiones: México Estados Unidos Europa Asia Cada región podría necesitar crear registros. Si todos dependen de una secuencia global perfectamente sincronizada, la arquitectura puede volverse más complicada. Con identificadores que pueden generarse independientemente: Servidor México → UUID Servidor Europa → UUID Servidor Asia → UUID la coordinación necesaria para crear IDs disminuye. 📴 También pueden generarse antes de llegar a la base de datos Este detalle es interesante. Con un ID autoincremental, muchas veces no sabes cuál será el identificador hasta después de insertar el registro. Por ejemplo: INSERT usuario ↓ Base de datos genera ID ↓ ID = 82 Con UUID puedes hacer: Generar UUID ↓ Construir objeto ↓ Enviar a base de datos El identificador ya existe antes del INSERT . Esto puede resultar útil cuando el dato pasa por varios sistemas antes de persistirse definitivamente. 🔄 Ejemplo con una cola de mensajes Supongamos que una aplicación crea un pedido. Antes de guardarlo necesita publicar eventos. Puede generar: pedido_id = 6eab76fc-... desde el principio. Ahora diferentes componentes pueden referirse al mismo pedido: Pedido creado ↓ UUID conocido ↓ Cola ↓ Inventario Pagos Notificaciones Todos comparten un identificador sin esperar a que una base central asigne uno. 🎲 ¿Cómo puede ser único? Un UUID tiene un espacio enorme de valores posibles. Al trabajar con 128 bits existen aproximadamente: 2^128 combinaciones posibles. Eso representa una cantidad extremadamente grande. Dependiendo de la versión utilizada, el UUID puede construirse usando información aleatoria, tiempo u otros componentes. La probabilidad de una colisión correctamente generada es extremadamente baja. ⚠️ "Único" no significa matemáticamente imposible de repetir Este detalle es importante. Decir que un UUID es universalmente único no significa que exista una garantía matemática absoluta de que jamás se repetirá. Especialmente en versiones basadas en aleatoriedad existe una probabilidad de colisión. Pero esa probabilidad es tan pequeña que para aplicaciones normales se considera prácticamente despreciable cuando los UUID se generan correctamente. Aun así, si el campo realmente debe ser único, la base de datos debería mantener una restricción: PRIMARY KEY o: UNIQUE La base de datos sigue siendo la última protección. 🧠 Existen diferentes versiones de UUID No todos los UUID se generan exactamente de la misma manera. Existen varias versiones. Algunas conocidas son: UUID v1 UUID v4 UUID v7 Cada una intenta resolver diferentes necesidades. 🎲 UUID v4 Una de las versiones más conocidas es UUID v4. Está basada principalmente en valores aleatorios. Un ejemplo: 8fa9c322-68da-4e57-a980-d429f981ad32 Su gran ventaja es que puede generarse fácilmente de forma independiente. No necesitas coordinación entre servidores. Por eso ha sido muy popular en aplicaciones distribuidas. ⏱️ UUID v1 UUID v1 incorpora información relacionada con tiempo y otros datos utilizados para generar el identificador. Esto permite que los valores tengan cierto componente temporal. Sin embargo, también tiene características relacionadas con privacidad y representación que hacen que no siempre sea la opción preferida para sistemas modernos. 🚀 UUID v7 UUID v7 es especialmente interesante para aplicaciones actuales. Combina información temporal con datos adicionales para producir identificadores que mantienen una propiedad importante: 👉 tienden a ordenarse aproximadamente por tiempo de creación. Por ejemplo, si generamos varios: UUID A UUID B UUID C los valores más recientes tienden a aparecer después en el orden. Esto puede ser útil para bases de datos e índices. 🧱 ¿Por qué el orden importa para los índices? Para entenderlo, imaginemos un índice como una estructura organizada. Con IDs consecutivos: 100 101 102 103 104 cuando insertamos: 105 normalmente el valor se agrega cerca del final. Eso es bastante conveniente para estructuras como árboles B-tree. 🎲 ¿Qué ocurre con UUID v4? Los UUID aleatorios pueden verse así: 7f... 12... a8... 45... de... Los nuevos valores pueden pertenecer a lugares completamente diferentes del índice. Conceptualmente: Índice ├── 12... ├── 45... ├── 7f... ├── a8... └── de... Cada inserción puede caer en una zona distinta. Eso puede aumentar: Fragmentación. Page splits. Escrituras aleatorias. Tamaño del índice. Uso de caché. El impacto real depende del motor, volumen de datos y configuración. Pero es una consideración importante. 🚀 Aquí UUID v7 puede ser una ventaja Como tiene un componente temporal, los nuevos identificadores tienden a estar cerca entre sí. Conceptualmente: 018f... 018f... 018f... 0190... Esto puede producir un comportamiento de inserción más amigable para ciertos índices que valores completamente aleatorios. Por eso las variantes ordenables han ganado interés en sistemas modernos. 💾 UUID ocupa más espacio Un entero puede ocupar relativamente poco. Por ejemplo: INT → 4 bytes BIGINT → 8 bytes Un UUID representa: 16 bytes si se guarda en formato binario. Si se almacena como una cadena textual: 550e8400-e29b-41d4-a716-446655440000 puede ocupar todavía más. Esto importa porque el identificador puede aparecer en: Clave primaria. Índices. Foreign Keys. Tablas relacionadas. En una base con millones o miles de millones de filas, esas diferencias pueden acumularse. ⚠️ Guardar UUID como texto no siempre es ideal Podríamos usar: VARCHAR(36) para guardar: 550e8400-e29b-41d4-a716-446655440000 Funciona. Pero algunos motores tienen tipos nativos de UUID o permiten utilizar almacenamiento binario más eficiente. Dependiendo del motor, puede ser preferible usar: UUID o: BINARY(16) en lugar de tratarlo simplemente como una cadena. 👀 Los IDs consecutivos son más fáciles de leer Hay otra diferencia práctica. Si alguien reporta: Problema con pedido 582 es muy cómodo. Puedes buscar: WHERE id = 582 En cambio: Problema con pedido 550e8400-e29b-41d4-a716-446655440000 es bastante menos agradable de leer, copiar o comunicar. Esto afecta tareas como: Soporte. Logs. Debugging. Operaciones manuales. Por eso algunos sistemas incluso mantienen dos identificadores. 🧩 ID interno y UUID externo Una estrategia posible es tener: id = 582 uuid = 550e8400-e29b-41d4-a716-446655440000 El ID numérico se utiliza internamente. El UUID se expone hacia el exterior. Por ejemplo: Base de datos: id = 582 Pero la API devuelve: /users/550e8400-e29b-41d4-a716-446655440000 Esto permite obtener ventajas de ambos modelos. Aunque también agrega complejidad y otro campo que mantener. 🔐 ¿UUID hace una API más segura? Aquí hay un error común. Imagina una API con: /users/1 /users/2 /users/3 Es evidente que existen IDs consecutivos. Alguien podría probar: /users/4 y descubrir otro registro si la aplicación tiene una falla de autorización. Con UUID: /users/6b37c5e4-... es mucho más difícil adivinar otro identificador válido. Eso puede reducir la enumeración trivial. Pero: 👉 UUID no reemplaza la autorización. Si un usuario puede acceder a un recurso solo porque conoce su UUID, el sistema sigue siendo inseguro. 🚨 Ejemplo de mala seguridad Supongamos: GET /facturas/{uuid} Y el backend hace: Buscar factura ↓ Encontrada ↓ Devolver sin comprobar a quién pertenece. El atacante quizá tenga más dificultad para adivinar UUIDs. Pero si consigue uno, podría acceder al dato. La validación correcta debe ser: Buscar factura ↓ ¿Pertenece al usuario autenticado? ↓ Sí → devolver No → rechazar La seguridad depende de autorización, no del aspecto del identificador. 🔎 Los IDs consecutivos también revelan cierta información Supongamos que creas una cuenta y recibes: id = 1,500,000 Eso puede dar una idea aproximada de cuántos registros han sido creados. Después recibes: id = 1,600,000 semanas más tarde. Podrías inferir cierto ritmo de crecimiento. Con identificadores aleatorios esta información no resulta tan evidente. En algunos sistemas eso puede ser una ventaja adicional. 🗄️ ¿Qué pasa al unir dos bases de datos? Este es otro escenario interesante. Imagina dos bases independientes. Base A: Pedido 1 Pedido 2 Pedido 3 Base B: Pedido 1 Pedido 2 Pedido 3 Si quieres combinar ambas, tienes colisiones inmediatamente. ¿Quién es Pedido 1? Con UUID: Base A → c125... Base B → 84da... los identificadores ya fueron generados para ser únicos globalmente. Eso puede simplificar procesos de integración o migración. 📴 Sistemas offline Supongamos que una aplicación móvil necesita crear información aunque no tenga Internet. Con una secuencia central tendría que preguntar al servidor: ¿Cuál es el siguiente ID? Pero está desconectada. Con UUID puede generar: nuevo_registro_uuid localmente. Después, cuando recupera conexión: Datos locales ↓ Sincronización ↓ Servidor El registro ya tiene un identificador válido. Esto puede ser muy útil en arquitecturas offline-first. 🌐 Microservicios Imagina varios servicios: Pedidos Pagos Envíos Inventario Cada uno podría necesitar identificar entidades sin depender continuamente de una misma base de datos. Un identificador global puede facilitar correlacionar información. Por ejemplo: pedido_id = UUID X puede aparecer en: Servicio de pedidos Servicio de pagos Servicio de envío Logs Eventos Esto ayuda a rastrear la misma entidad por diferentes componentes. 📊 Pero IDs consecutivos siguen funcionando perfectamente en muchos sistemas Aquí hay que evitar una idea equivocada: UUID = moderno y correcto INT = antiguo y malo No. Un ID autoincremental puede ser excelente. Por ejemplo, una aplicación interna con: 1 base de datos 1 backend volumen moderado sin generación distribuida probablemente no tenga ninguna necesidad real de utilizar UUID como clave primaria. Un: BIGINT AUTO_INCREMENT puede ser más sencillo y eficiente. 🤔 Entonces, ¿cuándo usar ID consecutivo? Puede ser una gran opción cuando: ✔️ Tienes una base central. ✔️ La generación distribuida no es un problema. ✔️ Quieres claves pequeñas. ✔️ Buscas buen comportamiento en índices. ✔️ La facilidad de lectura importa. ✔️ No existe una necesidad clara de identificadores globales. 🌎 ¿Cuándo puede convenir UUID? Puede ser útil cuando: ✔️ Múltiples servidores generan registros independientemente. ✔️ Los datos pueden crearse offline. ✔️ Varias bases pueden fusionarse. ✔️ Necesitas identificadores globalmente únicos. ✔️ Expones identificadores públicamente y quieres evitar secuencias predecibles. ✔️ Diferentes servicios necesitan generar IDs antes de persistir los datos. ✔️ Necesitas rastrear entidades entre sistemas distribuidos. ⚠️ UUID tampoco elimina todos los problemas distribuidos Utilizar UUID permite generar identificadores independientemente. Pero no soluciona automáticamente: Conflictos de datos Consistencia Duplicados lógicos Transacciones distribuidas Sincronización Por ejemplo, puedes generar dos UUID distintos: UUID A UUID B para representar accidentalmente la misma compra. Técnicamente los IDs son diferentes. Pero sigues teniendo un duplicado lógico. La unicidad del identificador no garantiza la unicidad del negocio. 🧠 Identidad técnica vs identidad de negocio Esta diferencia es muy importante. Podemos tener: UUID: 550e8400-... pero una operación también puede tener: numero_factura = F-2026-1052 El UUID identifica técnicamente el registro. El número de factura representa una identidad con significado dentro del negocio. No siempre deberían ser el mismo valor. 🔢 Tampoco tienes que elegir solamente entre INT y UUID Existen otras estrategias de identificadores distribuidos. Por ejemplo: ULID. Snowflake IDs. KSUID. UUID v7. Algunas intentan combinar: Unicidad + Orden temporal + Generación distribuida + Mejor comportamiento en índices La elección depende del sistema. ❄️ Snowflake IDs Un enfoque popularizado por sistemas distribuidos consiste en generar números grandes que incorporan información como tiempo y un identificador del nodo. Visualmente pueden parecer: 1541815603606036480 Siguen siendo números. Pero pueden generarse en múltiples servidores sin depender de una secuencia central tradicional. Esto demuestra que UUID no es la única solución para IDs distribuidos. 🧭 No elijas el identificador por moda Es fácil comenzar un proyecto y pensar: "Voy a utilizar UUID porque se ve más profesional." Pero la decisión debería responder preguntas reales: ¿Dónde se generan los registros? ¿Tenemos varios servidores? ¿Necesitamos modo offline? ¿El ID será público? ¿Cuántos registros tendremos? ¿Cómo afectará los índices? ¿Necesitamos orden temporal? Si la respuesta no justifica la complejidad adicional, quizá un ID secuencial sea mejor. 🛠️ Buenas prácticas ✔️ Utiliza el tipo de identificador que resuelva las necesidades reales del sistema. ✔️ No reemplaces IDs secuenciales únicamente por tendencia. ✔️ Considera el tamaño de los índices. ✔️ Utiliza tipos nativos o binarios para UUID cuando tu base de datos los soporte y sea apropiado. ✔️ Considera UUID v7 u otras alternativas ordenables cuando el comportamiento de los índices importe. ✔️ Mantén restricciones PRIMARY KEY o UNIQUE . ✔️ No utilices UUID como sustituto de autorización. ✔️ Separa identificadores técnicos de identificadores de negocio cuando tenga sentido. ✔️ Considera cómo se generarán los IDs antes de elegir una estrategia. 🧩 La realidad No existe un identificador universalmente mejor. Un ID secuencial: 582 puede ser perfecto para una aplicación centralizada. Un UUID: 550e8400-e29b-41d4-a716-446655440000 puede ser mucho más conveniente para un sistema donde diferentes componentes necesitan generar registros independientemente. La diferencia no está en cuál parece más moderno. Está en qué problema necesitas resolver. 💬 Los IDs consecutivos priorizan simplicidad y eficiencia. Los UUID priorizan la capacidad de generar identificadores prácticamente únicos sin depender de una secuencia central. Ambos siguen siendo herramientas perfectamente válidas. 👉 Antes de elegir entre BIGINT y UUID , pregúntate primero cómo y dónde se crearán los datos de tu sistema. Esa respuesta probablemente sea más importante que el formato del identificador. 🔥 El backend no se ve, pero sin él, nada funciona.

17 ago 2026
Leer publicación
¿Por qué algunas APIs pueden ser usadas por cualquier desarrollador... y otras solo funcionan dentro de una empresa?
APIs

¿Por qué algunas APIs pueden ser usadas por cualquier desarrollador... y otras solo funcionan dentro de una empresa?

Aquí conviene ampliar con autenticación, exposición en Internet, gateways, versionado, rate limits y APIs para partners, manteniendo clara la diferencia sin meternos demasiado en seguridad avanzada. 🚀 ¿Por qué algunas APIs pueden ser usadas por cualquier desarrollador... y otras solo funcionan dentro de una empresa? Imagina que quieres construir una aplicación. Necesitas mostrar mapas. Procesar pagos. Consultar el clima. Permitir iniciar sesión con una cuenta externa. O conectarte con otra plataforma. Muchas de estas integraciones son posibles gracias a una API . Pero hay algo importante: 👉 No todas las APIs están diseñadas para que cualquier persona pueda utilizarlas. Algunas existen precisamente para que desarrolladores externos construyan integraciones. Otras solamente sirven para conectar sistemas internos de una empresa. Y aunque técnicamente ambas pueden funcionar de manera muy parecida, su propósito, nivel de exposición y reglas de acceso son diferentes. 🧠 ¿Qué es una API? Una API es una interfaz que permite que dos sistemas se comuniquen. Por ejemplo: Aplicación móvil ↓ API ↓ Backend La aplicación puede solicitar información: GET /productos Y el servidor devuelve una respuesta. Por ejemplo: { "id": 15, "nombre": "Laptop", "precio": 18000 } La aplicación no necesita saber exactamente cómo está implementada la base de datos ni cómo funciona internamente el backend. Solo necesita conocer el contrato de la API. 🌐 ¿Qué es una API pública? Una API pública está diseñada para ser utilizada por desarrolladores o sistemas externos a la organización que la creó. Por ejemplo, una empresa puede ofrecer una API para permitir que otras aplicaciones: Procesen pagos. Consulten mapas. Accedan a repositorios. Envíen mensajes. Consulten productos. Integren funcionalidades externas. Normalmente una API pública cuenta con: 📚 Documentación. 🔑 Mecanismos de autenticación. 📊 Límites de uso. 📌 Versionado. ⚠️ Políticas y restricciones. El objetivo es permitir integraciones externas de forma controlada. 🚀 Algunos ejemplos conocidos Existen plataformas que ofrecen APIs para desarrolladores. Por ejemplo: Google Maps Stripe GitHub Un desarrollador puede consultar su documentación, obtener credenciales y comenzar a utilizar ciertas funcionalidades desde su propia aplicación. Conceptualmente: Tu aplicación ↓ API pública ↓ Servicio externo Esto permite aprovechar capacidades que sería mucho más complejo construir desde cero. 🔐 Pública no significa "sin seguridad" Este es un error muy común. Una API pública no significa: Cualquiera puede hacer cualquier cosa Significa que está diseñada para ser consumida externamente. Pero normalmente necesita controles. Por ejemplo: API Key OAuth Tokens Firmas Permisos Una API puede estar disponible desde Internet y aun así exigir autenticación. Por ejemplo: Authorization: Bearer TOKEN Si el token no es válido: 401 Unauthorized 🧠 ¿Qué es una API privada? Una API privada , también llamada interna, está pensada para ser utilizada únicamente por los propios sistemas de una organización. Por ejemplo: Aplicación web ↓ API interna ↓ Servicio de inventario O: Servicio de pedidos ↓ API interna ↓ Servicio de pagos interno Los usuarios externos ni siquiera necesitan saber que esa API existe. 🏢 Un ejemplo dentro de una empresa Imagina una tienda en línea. La empresa tiene: Sitio web Aplicación móvil Sistema administrativo Sistema de inventario Todos necesitan consultar productos. En lugar de implementar la lógica de productos cuatro veces, podrían utilizar una API interna: ┌→ Página web │ API Productos ─→ App móvil │ ├→ Administración │ └→ Inventario Todos utilizan la misma fuente de información. Pero esa API puede estar disponible solamente dentro de la infraestructura de la empresa. ⚙️ Técnicamente pueden parecer muy similares Una API pública podría tener: GET /api/v1/products Y una privada podría tener exactamente algo parecido: GET /internal/products Ambas pueden utilizar: HTTP JSON REST GraphQL gRPC La principal diferencia no siempre está en la tecnología. Está en: Quién puede utilizarla + Para qué fue diseñada + Cómo está expuesta 🌎 La exposición también cambia Una API pública normalmente necesita estar accesible desde Internet. Por ejemplo: Internet ↓ api.empresa.com Mientras que una API privada puede vivir exclusivamente dentro de una red interna. Por ejemplo: Internet ↓ Frontend público ↓ Red interna ↓ API inventario El usuario externo nunca puede comunicarse directamente con el servicio de inventario. Solo otros componentes autorizados dentro de la infraestructura. 🛡️ ¿Por qué mantener APIs privadas? Porque no toda funcionalidad necesita estar expuesta al exterior. Supongamos que existe: POST /recalcular-inventario Ese endpoint solamente lo utiliza un proceso interno. Publicarlo innecesariamente aumenta la superficie de ataque y también obliga a soportar consumidores externos que quizá nunca fueron necesarios. Mantenerlo privado permite controlar mejor quién puede acceder. 🚀 Un ejemplo completo Imagina un ecommerce. Tenemos: Página web App móvil Sistema ERP Proveedores externos La empresa podría tener una API privada para: Consultar inventario interno Actualizar existencias Administrar proveedores Generar reportes Y una API pública separada para permitir: Consultar productos autorizados Crear pedidos Consultar estado de órdenes Los dos tipos de API pueden convivir. 🤝 También existen APIs para partners La división pública/privada no siempre es totalmente binaria. Existe también un escenario intermedio. Por ejemplo: API para socios comerciales No está disponible para cualquier desarrollador. Pero tampoco es exclusivamente interna. Solamente pueden utilizarla empresas previamente autorizadas. Por ejemplo: Empresa A ✅ Empresa B ✅ Empresa C ✅ Cualquier usuario ❌ A veces se les llama Partner APIs . 🧩 Entonces podemos pensar en tres niveles De forma simplificada: API privada ↓ Solo sistemas internos API partner ↓ Socios autorizados API pública ↓ Desarrolladores externos Cada nivel requiere diferentes controles. 🔑 ¿Cómo se controla quién entra? Existen diferentes mecanismos de autenticación. Uno bastante común es una: API Key Por ejemplo: X-API-Key: ABC123XYZ El servidor verifica esa clave antes de procesar la solicitud. 🔐 También puede utilizarse OAuth Cuando necesitas permisos más avanzados, puede utilizarse OAuth. Por ejemplo, el clásico botón: "Iniciar sesión con Google" Tu aplicación no recibe directamente la contraseña del usuario. En su lugar existe un flujo de autorización. Conceptualmente: Usuario ↓ Google ↓ Autoriza aplicación ↓ Token ↓ Tu aplicación Ese token puede utilizarse para acceder únicamente a ciertos recursos permitidos. 🎯 Autenticación y autorización no son lo mismo Aunque están relacionadas, representan cosas diferentes. Autenticación Pregunta: ¿Quién eres? Autorización Pregunta: ¿Qué puedes hacer? Por ejemplo, dos aplicaciones pueden estar autenticadas correctamente. Pero una puede tener permiso de: READ y otra: READ WRITE Eso es especialmente importante en APIs públicas. 🚦 Rate Limiting Una API pública también necesita protegerse frente al uso excesivo. Imagina que un desarrollador envía: 1,000,000 solicitudes por segundo Eso podría afectar a todos los usuarios. Por eso pueden existir límites. Por ejemplo: 100 solicitudes por minuto Si se supera: 429 Too Many Requests Esto permite distribuir recursos de forma más controlada. 📊 Diferentes clientes pueden tener distintos límites Por ejemplo: Plan gratuito 100 requests/minuto Plan empresarial 10,000 requests/minuto O incluso límites dependiendo del endpoint. Una API pública necesita pensar en este tipo de escenarios porque no controla completamente cómo los clientes utilizarán el servicio. 📚 La documentación es mucho más importante en una API pública Si construyes una API privada y tienes una duda, quizá puedas preguntarle directamente al equipo responsable. Pero un desarrollador externo no debería depender de eso. Necesita documentación que explique: Endpoints Parámetros Autenticación Respuestas Errores Ejemplos Límites Por ejemplo: GET /v1/products/{id} La documentación debería explicar: Qué hace. Qué parámetros recibe. Qué devuelve. Qué errores puede producir. 📝 El contrato de una API pública es muy importante Cuando miles de desarrolladores dependen de una API, hacer cambios puede ser peligroso. Supongamos que actualmente devuelve: { "name": "Laptop" } Y mañana simplemente cambias: { "product_name": "Laptop" } Todas las aplicaciones que esperaban: name pueden dejar de funcionar. Por eso las APIs públicas necesitan mucho cuidado con los cambios. 🔢 Aquí entra el versionado Una estrategia común consiste en utilizar versiones. Por ejemplo: /api/v1/products Después puede aparecer: /api/v2/products Esto permite introducir cambios importantes sin romper inmediatamente a todos los consumidores de la versión anterior. Naturalmente, mantener múltiples versiones también tiene un costo. Por eso los cambios deben planearse con cuidado. ⚠️ Una API privada también necesita estabilidad Que una API sea interna no significa que puedas romperla constantemente. Podría haber: 20 servicios dependiendo de ella. Cambiar un campo sin coordinación puede romper media plataforma. La diferencia es que dentro de una empresa normalmente existe mayor control sobre quién consume la API y cuándo puede actualizarse. 🚪 ¿Qué es un API Gateway? En sistemas grandes, las solicitudes externas pueden pasar primero por un API Gateway . Conceptualmente: Internet ↓ API Gateway ↓ ├── Usuarios ├── Pedidos ├── Pagos └── Productos El gateway puede encargarse de tareas como: Autenticación. Rate limiting. Enrutamiento. Logs. Métricas. Validaciones. Esto evita exponer directamente todos los servicios internos. 🛡️ Un gateway ayuda a esconder la arquitectura interna Imagina que internamente existen: users-service payments-service inventory-service orders-service No necesariamente quieres que un consumidor externo conozca esas direcciones. Puedes exponer simplemente: api.empresa.com Y dejar que el gateway decida hacia dónde enviar cada petición. Por ejemplo: /users → users-service /orders → orders-service /payments → payments-service 🧩 APIs públicas e internas pueden tener contratos diferentes Un error sería exponer directamente la API interna solo porque ya existe. Supongamos que internamente manejas: { "product_id": 125, "warehouse_id": 8, "supplier_internal_code": "A782", "cost": 100, "margin": 0.25 } Quizá un consumidor externo solamente debería ver: { "id": 125, "name": "Producto", "price": 125 } La API pública debería exponer únicamente la información que realmente necesita el consumidor. 🚨 No expongas información interna innecesariamente Una API pública no debería revelar datos como: IDs internos sensibles. Información administrativa. Campos de auditoría. Datos de infraestructura. Credenciales. Información privada. Siempre debería aplicarse el principio: 👉 Exponer únicamente lo necesario. ⚙️ Diferentes requisitos de rendimiento Las APIs internas y públicas también pueden tener necesidades diferentes. Una API privada puede estar diseñada para: Sistema A ↓ Sistema B con tráfico altamente predecible. Una API pública podría recibir solicitudes desde: Aplicación A Aplicación B Aplicación C ... Aplicación 10,000 Esto puede requerir controles adicionales de capacidad y abuso. 📈 Una API pública puede convertirse en un producto Para algunas empresas, la API no es simplemente una herramienta técnica. Es parte del negocio. Por ejemplo, otras empresas pueden pagar para utilizarla. En ese caso aparecen conceptos como: Planes Cuotas Facturación SLA Soporte Analítica La API se trata prácticamente como cualquier otro producto. 📝 ¿Qué es un SLA? SLA significa: Service Level Agreement Puede definir compromisos relacionados con cosas como: Disponibilidad Tiempo de respuesta Soporte Por ejemplo: Disponibilidad objetivo: 99.9% Esto es especialmente importante cuando otras empresas dependen de tu API para operar. 🔍 Observabilidad Tanto las APIs públicas como las privadas deberían estar monitoreadas. Conviene observar: Latencia Errores Requests por segundo Endpoints más utilizados Consumo por cliente Pero en una API pública también puede interesarnos conocer: Qué desarrollador consume más Qué versión utiliza Qué errores recibe Si supera sus límites Esto ayuda a operar el servicio de forma segura. ⚠️ Error común: "privada" significa segura Que una API solamente exista en una red interna no significa que podamos olvidar la seguridad. Los sistemas internos también necesitan controles. Por ejemplo: Servicio de reportes no necesariamente debería poder: Eliminar usuarios Modificar pagos Actualizar inventario solo porque ambos están dentro de la misma empresa. Las APIs privadas también deberían utilizar autenticación, autorización y permisos adecuados cuando el contexto lo requiera. 🛡️ La red interna no debería ser la única defensa Pensar: "Está dentro de mi red, entonces es seguro." puede generar problemas. Si un componente interno es comprometido, podría intentar acceder a otros servicios. Por eso arquitecturas modernas suelen aplicar controles incluso entre servicios internos. 🚀 Un ejemplo completo Imagina una plataforma de ecommerce. Internamente tenemos: Web App móvil Pedidos Inventario Pagos Administración Todos estos sistemas se comunican mediante APIs privadas. Después la empresa decide permitir que terceros consulten su catálogo. No necesita exponer directamente su API interna. Puede crear: Public API que ofrece únicamente: GET /products GET /products/{id} GET /categories con autenticación, rate limiting y documentación. Ahora tenemos: Sistemas internos ↓ APIs privadas Desarrolladores externos ↓ API pública Cada una diseñada para un propósito diferente. 🧠 Entonces, ¿cuándo hacer pública una API? Tiene sentido cuando realmente queremos permitir que terceros construyan integraciones. Por ejemplo: ✔️ Ofrecer una plataforma. ✔️ Permitir integraciones con socios. ✔️ Crear un ecosistema de desarrolladores. ✔️ Automatizar procesos externos. ✔️ Exponer determinadas capacidades como producto. Pero no todo endpoint interno necesita convertirse en público. 🛠️ Buenas prácticas ✔️ Expón únicamente las funcionalidades que realmente necesitan consumidores externos. ✔️ Mantén separados los contratos internos y externos cuando tenga sentido. ✔️ Utiliza autenticación y autorización apropiadas. ✔️ Aplica rate limiting en APIs expuestas. ✔️ Documenta correctamente las APIs públicas. ✔️ Diseña una estrategia de versionado. ✔️ No expongas datos internos innecesarios. ✔️ Monitorea tráfico, errores y latencia. ✔️ Protege también las APIs privadas. ✔️ Utiliza un API Gateway cuando la arquitectura lo necesite. 🧩 La realidad Una API pública y una API privada pueden utilizar exactamente las mismas tecnologías. Ambas pueden hablar HTTP. Ambas pueden devolver JSON. Ambas pueden utilizar tokens. La diferencia principal está en su propósito. Una API pública está diseñada para que sistemas externos puedan integrarse de forma controlada. Una API privada permite que los propios sistemas de una organización colaboren entre ellos. Y muchas aplicaciones modernas necesitan ambas. 💬 Una buena arquitectura no consiste en hacer públicos todos los endpoints que existen. Consiste en decidir claramente: Quién necesita acceder ↓ Qué necesita consultar ↓ Qué puede modificar ↓ Qué información debe permanecer interna 👉 La próxima vez que utilices una API desde tu aplicación, recuerda que probablemente detrás de esa API pública existen muchas otras APIs privadas que nunca serán visibles para ti. 🔥 El backend no se ve, pero sin él, nada funciona.

16 ago 2026
Leer publicación
¿Cómo logran comunicarse dos sistemas... sin depender de que ambos estén disponibles al mismo tiempo?
Arquitectura de Software

¿Cómo logran comunicarse dos sistemas... sin depender de que ambos estén disponibles al mismo tiempo?

Imagina que una tienda en línea recibe una compra. El pago fue aprobado. El pedido quedó registrado. Pero todavía faltan varias tareas: 📧 Enviar un correo de confirmación. 📦 Actualizar el inventario. 🧾 Generar una factura. 🔔 Crear una notificación. 📊 Registrar información para reportes. Todo parece formar parte de la misma operación. Pero surge un problema: 👉 ¿Qué pasa si el servicio de correos está caído durante cinco minutos? ¿La compra también debería fallar? ¿El usuario debería intentar pagar de nuevo? ¿Toda la aplicación tendría que esperar? En muchos casos, la respuesta es no . Y precisamente para evitar ese tipo de dependencia existen las colas de mensajes . 🧠 ¿Qué es una cola de mensajes? Una cola de mensajes es un mecanismo que permite que dos sistemas se comuniquen sin necesidad de hacerlo directamente ni al mismo tiempo. En lugar de esto: Sistema A ↓ Sistema B ↓ Esperar respuesta podemos tener: Sistema A ↓ Cola ↓ Sistema B El primer sistema deja un mensaje. El segundo lo procesa cuando puede. Así ambos quedan menos acoplados. 📩 ¿Qué contiene un mensaje? Un mensaje normalmente representa algo que ocurrió o algo que debe procesarse. Por ejemplo: { "pedido_id": 8421, "evento": "pedido_confirmado" } Ese mensaje puede indicar: "El pedido 8421 fue confirmado." Después otro componente puede utilizar esa información para enviar un correo, generar una factura o realizar cualquier tarea relacionada. La idea importante es que el productor no necesita ejecutar directamente todo ese trabajo. ⚙️ Productores y consumidores En una arquitectura basada en colas aparecen normalmente dos roles. Productor Es quien crea y envía el mensaje. Por ejemplo: Servicio de pedidos Puede publicar: pedido_confirmado Consumidor Es quien recibe y procesa el mensaje. Por ejemplo: Servicio de correos puede tomar ese mensaje y enviar la confirmación correspondiente. Conceptualmente: Pedido ↓ Productor ↓ Cola ↓ Consumidor ↓ Enviar correo 🚀 Un ejemplo completo: una compra Imagina que el usuario realiza un pedido. Primero necesitamos completar lo realmente importante: Usuario compra ↓ Validar pago ↓ Guardar pedido Una vez confirmado, la aplicación puede publicar un mensaje: pedido_confirmado Ahora otros procesos pueden reaccionar. Por ejemplo: pedido_confirmado ↓ ├── Enviar correo ├── Generar factura └── Registrar analítica La compra ya no necesita esperar a que todas esas tareas terminen. ⏱️ Comunicación síncrona vs asíncrona Esta diferencia es fundamental. En una comunicación síncrona , un sistema espera al otro. Por ejemplo: Servicio A ↓ Llama a Servicio B ↓ Espera ↓ Servicio B responde ↓ Servicio A continúa Si B tarda 10 segundos, A espera 10 segundos. Si B no responde, A tiene que manejar el error. En una comunicación asíncrona : Servicio A ↓ Publica mensaje ↓ Continúa El consumidor procesa ese mensaje posteriormente. Esto permite separar mejor los tiempos de ejecución. 💥 ¿Qué pasa si el consumidor está caído? Aquí aparece una de las principales ventajas. Supongamos que el servicio de correo deja de funcionar. Sin cola: Compra ↓ Enviar correo ↓ ❌ Servicio caído ↓ ¿Falla compra? Con cola: Compra ↓ Guardar mensaje ↓ ✅ Compra continúa Servicio de correo ↓ ❌ Temporalmente caído El mensaje permanece pendiente. Cuando el consumidor vuelve: Servicio vuelve ↓ Lee mensajes pendientes ↓ Envía correos Esto permite tolerar fallos temporales. 📦 La cola actúa como un buffer También puede funcionar como un amortiguador entre dos sistemas con velocidades distintas. Supongamos que la aplicación genera: 1,000 tareas por segundo pero un consumidor solo puede procesar: 500 tareas por segundo Sin una cola, el consumidor podría saturarse. Con una cola: Productores ↓ 1000 msg/s ↓ COLA ↓ 500 msg/s ↓ Consumidor Los mensajes restantes permanecen esperando. Esto permite absorber picos temporales de tráfico. 📈 Ejemplo durante una promoción Imagina que una tienda lanza una oferta. Normalmente recibe: 100 pedidos por minuto Pero durante la promoción llega a: 5,000 pedidos por minuto Si cada pedido intenta enviar un correo inmediatamente, podrías saturar el proveedor. Una cola permite acumular temporalmente ese trabajo. Pedidos ↓ Cola de emails ↓ Workers ↓ Proveedor de correo Los workers procesan los mensajes a una velocidad controlada. 👷 ¿Qué es un worker? Un worker es un proceso encargado de tomar trabajos pendientes y ejecutarlos. Por ejemplo: Cola ↓ Worker 1 Worker 2 Worker 3 Cada uno puede tomar mensajes diferentes. Esto permite aumentar capacidad agregando más workers. Por ejemplo: 1 worker ↓ 100 mensajes/s Si necesitamos más capacidad: 5 workers ↓ ~500 mensajes/s dependiendo naturalmente del tipo de tarea y de la infraestructura. 🚀 Escalar consumidores Este es otro beneficio importante. El productor puede seguir funcionando igual aunque aumentemos el número de consumidores. Antes: Cola ↓ Worker 1 Después: Cola ├── Worker 1 ├── Worker 2 ├── Worker 3 └── Worker 4 El sistema puede procesar más mensajes sin cambiar necesariamente la lógica que los genera. ✅ ¿Cómo sabe la cola que un mensaje fue procesado? Aquí aparecen las confirmaciones , frecuentemente llamadas acknowledgements. Supongamos: Cola ↓ Mensaje 123 ↓ Worker El worker procesa correctamente el mensaje. Después confirma: ACK Entonces el sistema sabe que ese trabajo fue completado. Conceptualmente: Mensaje ↓ Consumidor ↓ Procesar ↓ ✅ ACK ↓ Eliminar de pendientes 💥 ¿Qué pasa si el worker falla a mitad del proceso? Supongamos que toma el mensaje: Enviar email 8421 Pero el proceso se apaga antes de confirmar. En sistemas configurados para ello, el mensaje puede volver a quedar disponible para otro consumidor. Worker 1 toma mensaje ↓ ❌ Worker 1 falla ↓ Mensaje vuelve a estar disponible ↓ Worker 2 lo procesa Esto ayuda a evitar pérdida de trabajo. 🔁 Pero aparece otro problema: mensajes duplicados Si un mensaje puede volver a procesarse, debemos asumir algo importante: 👉 Un consumidor podría recibir el mismo mensaje más de una vez. Por ejemplo: Enviar factura del pedido 50 Worker procesa la factura. Pero falla antes de enviar la confirmación. El mensaje vuelve a la cola. Otro worker lo toma. Ahora podría generar la factura nuevamente. Por eso los consumidores deben diseñarse con cuidado. 🔐 Aquí vuelve a aparecer la idempotencia Un consumidor idempotente puede procesar varias veces el mismo mensaje sin generar efectos duplicados. Por ejemplo: Mensaje: generar_factura pedido_50 Antes de generar: ¿Pedido 50 ya tiene factura? Si sí: No crear otra Esto protege contra duplicados. Las colas y la idempotencia suelen ir muy de la mano. 🔁 Reintentos Algunas tareas pueden fallar temporalmente. Por ejemplo: Enviar correo ↓ Proveedor responde 503 No necesariamente queremos descartar el mensaje. Podemos intentar nuevamente. Por ejemplo: Intento 1 ↓ Error Esperar Intento 2 ↓ Error Esperar Intento 3 ↓ ✅ Correcto Esto funciona bien cuando el fallo es temporal. ⚠️ Reintentar infinitamente tampoco es buena idea Imagina un mensaje inválido: pedido_id = inexistente Si el consumidor hace: falló ↓ retry ↓ falló ↓ retry ↓ falló ↓ retry puede quedarse atrapado indefinidamente. Por eso normalmente se define: Máximo de intentos Después de superar ese límite, se necesita otra estrategia. ☠️ Dead Letter Queue Una estrategia común es utilizar una Dead Letter Queue , o DLQ. Los mensajes que no pudieron procesarse correctamente después de varios intentos se envían a una cola especial. Por ejemplo: Cola principal ↓ Mensaje ↓ Error ↓ Retry ↓ Error ↓ Retry ↓ Error ↓ DLQ La DLQ permite investigar esos casos sin bloquear continuamente el flujo principal. 🔍 ¿Para qué sirve una DLQ? Puede ayudarte a identificar: Mensajes mal formados. Datos inválidos. Errores de lógica. Dependencias que llevan mucho tiempo fallando. Problemas específicos que requieren intervención. En lugar de perder el mensaje, queda disponible para análisis. 📚 ¿Se mantiene el orden de los mensajes? Depende de la tecnología y de cómo esté configurada. Imagina: 1. Crear pedido 2. Cancelar pedido Si los mensajes se procesan en orden contrario: Cancelar pedido Crear pedido podrías terminar con un estado incorrecto. Por eso hay procesos donde el orden es importante. Pero mantener orden estricto también puede limitar la capacidad de procesamiento paralelo. Es otra decisión de arquitectura. 🧠 No todos los mensajes necesitan orden global Por ejemplo: Correo para usuario A Correo para usuario B probablemente no importa cuál se procesa primero. Pero: Actualizar saldo +100 Actualizar saldo -50 puede requerir más cuidado. La estrategia depende completamente de la lógica del negocio. 📣 Evento vs comando Aunque muchas veces ambos viajan mediante sistemas de mensajería, representan ideas diferentes. Un comando suele decir: Haz esto. Por ejemplo: EnviarCorreoDeConfirmacion Un evento suele indicar: Esto ocurrió. Por ejemplo: PedidoConfirmado Entonces varios consumidores pueden reaccionar al mismo evento. 🚀 Un evento puede activar varios procesos Por ejemplo: PedidoConfirmado ↓ ├── Email ├── Facturación ├── Analítica └── Notificaciones El servicio de pedidos no necesita conocer los detalles de cada consumidor. Solo publica que ocurrió algo. Esto reduce mucho el acoplamiento. 🧩 Desacoplar no significa eliminar dependencias Este punto es importante. La cola reduce dependencias directas. Pero los sistemas siguen estando relacionados desde el punto de vista del negocio. Por ejemplo: Pedido confirmado ↓ Factura necesaria La diferencia es que ya no necesitas una llamada directa en ese mismo instante. El acoplamiento temporal disminuye. ⏳ Acoplamiento temporal Sin cola: A necesita que B esté disponible ahora Con cola: A publica B procesa cuando esté disponible Esa diferencia es muy importante en sistemas distribuidos. Porque la disponibilidad de un componente ya no determina necesariamente la disponibilidad inmediata del otro. 🐇 RabbitMQ RabbitMQ es una tecnología muy utilizada para mensajería. Suele trabajar con conceptos como: Producer Exchange Queue Consumer Un productor publica mensajes. Un exchange ayuda a decidir hacia qué colas deben dirigirse. Después los consumidores procesan los mensajes. Es especialmente común en procesamiento de tareas, integración entre servicios y sistemas donde necesitamos mecanismos clásicos de colas. 🌊 Apache Kafka Kafka también se utiliza para comunicación asíncrona, aunque su modelo tiene diferencias importantes. Kafka está muy orientado a flujos de eventos. Los mensajes se organizan en topics y pueden conservarse durante un periodo determinado. Por ejemplo: Topic: pedidos puede contener: PedidoCreado PedidoPagado PedidoEnviado PedidoCancelado Diferentes consumidores pueden leer esos eventos para distintos propósitos. ⚠️ RabbitMQ y Kafka no son exactamente lo mismo Aunque ambos aparecen frecuentemente cuando hablamos de mensajería, no deberían considerarse simplemente intercambiables. Cada uno tiene características distintas relacionadas con: Entrega. Persistencia. Consumo. Orden. Retención. Escalabilidad. Modelo de procesamiento. La elección depende del problema que estés resolviendo. 🚫 Una cola tampoco es una solución mágica Agregar una cola introduce nuevas responsabilidades. Antes: Servicio A ↓ Servicio B Ahora: Servicio A ↓ Broker ↓ Servicio B Tenemos otra pieza que operar. Eso significa pensar en: Disponibilidad. Persistencia. Monitoreo. Duplicados. Reintentos. Mensajes fallidos. Capacidad. Latencia. La arquitectura se vuelve más resistente en ciertos escenarios, pero también más compleja. 📊 El tamaño de la cola es una métrica importante Supongamos que normalmente tienes: 100 mensajes pendientes Pero de repente: 100 ↓ 1,000 ↓ 10,000 ↓ 100,000 Probablemente hay un problema. Quizá: Los consumidores están caídos. Procesan demasiado lento. Aumentó el tráfico. Existe una dependencia externa fallando. Por eso es importante monitorear la profundidad de la cola. ⏱️ También importa la antigüedad de los mensajes Tener: 10,000 mensajes no siempre es malo si se procesan rápidamente. Pero tener: Mensaje más antiguo: 45 minutos cuando normalmente tarda segundos puede indicar un problema serio. Por eso conviene observar: Cantidad pendiente + Edad de mensajes + Velocidad de producción + Velocidad de consumo 📈 Backpressure Cuando los productores generan trabajo más rápido de lo que los consumidores pueden procesar, aparece presión sobre el sistema. La cola puede absorber temporalmente esa diferencia. Pero no infinitamente. Por ejemplo: Producción = 1,000 msg/s Consumo = 500 msg/s Cada segundo quedan: +500 mensajes pendientes Después de una hora serían muchísimos mensajes acumulados. Por eso una cola ayuda con picos temporales, pero no resuelve una capacidad insuficiente permanente. 🧪 ¿Qué deberías probar? No basta con probar: Publicar mensaje ↓ Procesarlo correctamente También deberías probar escenarios como: Consumidor caído Broker reiniciado Mensaje duplicado Mensaje inválido Dependencia externa lenta Worker falla después de procesar Cola crece rápidamente Ahí es donde realmente descubrimos si el sistema es resistente. ⚠️ Error común: poner absolutamente todo en una cola No toda comunicación necesita ser asíncrona. Por ejemplo, si un usuario consulta: ¿Cuál es mi saldo? necesita una respuesta inmediata. No tendría sentido decir: Tu solicitud está en una cola. Quizá recibas tu saldo después. Hay operaciones que naturalmente requieren comunicación síncrona. Las colas son especialmente útiles cuando el trabajo puede completarse posteriormente. 🚀 Buenos candidatos para una cola Por ejemplo: ✔️ Enviar correos. ✔️ Generar reportes. ✔️ Procesar imágenes. ✔️ Generar facturas. ✔️ Ejecutar notificaciones. ✔️ Procesar eventos. ✔️ Sincronizar sistemas. ✔️ Ejecutar tareas pesadas. ✔️ Manejar trabajo que puede reintentarse. No significa que siempre deban utilizar una cola, pero son escenarios comunes. 🛠️ Buenas prácticas ✔️ Diseña consumidores capaces de manejar mensajes duplicados. ✔️ Utiliza confirmaciones para evitar perder trabajo. ✔️ Implementa reintentos limitados. ✔️ Considera una Dead Letter Queue para mensajes problemáticos. ✔️ Monitorea la cantidad y antigüedad de mensajes pendientes. ✔️ Define claramente qué datos necesita cada mensaje. ✔️ Evita enviar información innecesariamente grande. ✔️ Decide si el orden es importante para cada flujo. ✔️ Escala consumidores cuando la producción supera su capacidad. ✔️ No utilices comunicación asíncrona cuando realmente necesitas una respuesta inmediata. 🧩 La realidad Las colas de mensajes no existen únicamente para hacer que una aplicación responda más rápido. Su principal valor está en separar responsabilidades y tiempos de ejecución. Permiten transformar algo como: Compra ↓ Pago ↓ Correo ↓ Factura ↓ Notificación ↓ Respuesta en algo más parecido a: Compra ↓ Pago ↓ Guardar pedido ↓ Publicar eventos ↓ Responder Y después: Cola ├── Correo ├── Factura ├── Notificación └── Analítica Cada proceso puede trabajar de forma independiente. Si uno falla temporalmente, los demás no necesariamente tienen que detenerse. 💬 Un sistema distribuido no necesita que todos sus componentes estén disponibles exactamente al mismo tiempo. Necesita mecanismos que permitan conservar el trabajo pendiente y procesarlo cuando cada componente esté preparado. 👉 La próxima vez que una aplicación complete una operación inmediatamente pero el correo llegue unos segundos después, probablemente ese trabajo haya sido procesado de forma asíncrona. 🔥 El backend no se ve, pero sin él, nada funciona.

15 ago 2026
Leer publicación
¿Cómo puede una página web verse completamente diferente... sin cambiar una sola línea de HTML?
Frontend

¿Cómo puede una página web verse completamente diferente... sin cambiar una sola línea de HTML?

Entras a una página web. La ves desde tu computadora. Después la abres desde tu celular. Todo cambia. El menú se acomoda distinto. Las columnas desaparecen. Los botones cambian de tamaño. Las imágenes se adaptan. Más tarde activas el modo oscuro. Y nuevamente toda la apariencia cambia. Sin embargo, el contenido sigue siendo prácticamente el mismo. El título continúa ahí. El texto continúa ahí. Las imágenes siguen siendo las mismas. Entonces surge una pregunta: 👉 ¿Cómo puede cambiar tanto una página sin modificar su estructura? La respuesta está en CSS . 🧠 ¿Qué es CSS? CSS significa: Cascading Style Sheets o en español: Hojas de Estilo en Cascada . Es el lenguaje encargado de definir cómo se presenta visualmente una página web. Mientras HTML describe la estructura y el contenido, CSS define su apariencia. Por ejemplo, HTML puede decir: <h1>Mi tienda</h1> <button>Comprar</button> Eso solamente define que existe: Un título. Un botón. Pero CSS puede decidir que el título sea grande, rojo y centrado. Y que el botón tenga fondo oscuro, bordes redondeados y cierto espacio alrededor. Por ejemplo: h1 { color: red; text-align: center; } button { background: black; color: white; border-radius: 8px; } La estructura HTML no cambió. Solo cambió la forma en que el navegador la muestra. 🧩 HTML y CSS tienen responsabilidades distintas Una forma sencilla de entenderlo es esta: HTML ↓ ¿Qué existe? CSS ↓ ¿Cómo se ve? HTML puede definir: Título Párrafo Imagen Botón Formulario CSS puede decidir: Color Tamaño Posición Espaciado Tipografía Animación Distribución Esa separación hace que una misma estructura pueda utilizar estilos completamente diferentes. ⚙️ ¿Cómo aplica CSS el navegador? Cuando el navegador recibe una página, interpreta primero el HTML y construye su estructura. Después procesa las reglas de CSS. Por ejemplo: <p class="descripcion"> Producto disponible </p> Y CSS: .descripcion { color: green; font-size: 18px; } El navegador relaciona la clase: descripcion con las reglas correspondientes. Entonces sabe que ese párrafo debe mostrarse en verde y con determinado tamaño. 🎯 Los selectores permiten elegir qué modificar CSS necesita alguna forma de saber a qué elementos aplicar cada estilo. Para eso utiliza selectores . Por ejemplo: p { color: gray; } aplica el estilo a todos los párrafos. También podemos usar una clase: .destacado { color: red; } O un identificador: #titulo-principal { font-size: 48px; } Así podemos modificar grupos completos de elementos o elementos específicos sin alterar su contenido. 🚀 Un mismo HTML puede tener dos diseños completamente distintos Imagina este HTML: <div class="producto"> <h2>Laptop</h2> <p>$15,000</p> <button>Comprar</button> </div> Podemos crear un diseño claro: .producto { background: white; color: black; } O uno oscuro: .producto { background: #111; color: white; } El HTML sigue siendo exactamente el mismo. Pero visualmente parecen dos interfaces diferentes. 🌙 Así funciona el modo oscuro El modo claro y el modo oscuro son un excelente ejemplo. Podrías tener: body { background: white; color: black; } Y para modo oscuro: body.dark { background: #111; color: white; } Cuando el usuario activa el modo oscuro, JavaScript puede agregar una clase: dark al documento. El navegador vuelve a aplicar las reglas correspondientes. Conceptualmente: Modo claro ↓ body Modo oscuro ↓ body.dark El contenido no cambia. Solo cambian los estilos que se aplican. 📱 ¿Y cómo sabe CSS que estás en un celular? Aquí entran las media queries . Permiten aplicar reglas dependiendo de características de la pantalla. Por ejemplo: .productos { display: grid; grid-template-columns: repeat(4, 1fr); } En escritorio podríamos mostrar cuatro columnas. Pero después: @media (max-width: 768px) { .productos { grid-template-columns: 1fr; } } Cuando la pantalla es pequeña, la misma lista puede mostrarse en una sola columna. Entonces tenemos: Escritorio ↓ [1][2][3][4] Celular ↓ [1] [2] [3] [4] Mismos elementos. Diferente presentación. 📐 Esto se conoce como Responsive Design El diseño responsivo permite que una interfaz se adapte a diferentes tamaños de pantalla. Una página moderna puede utilizarse desde: 💻 Computadora. 📱 Teléfono. 📟 Tablet. 🖥️ Monitor grande. Sin crear necesariamente una página HTML completamente diferente para cada dispositivo. CSS reorganiza los elementos según el espacio disponible. 🧱 CSS también controla la distribución CSS no solamente cambia colores. Puede controlar completamente cómo se organiza una página. Por ejemplo, con: display: flex; podemos utilizar Flexbox . Esto permite organizar elementos fácilmente: [Logo] [Menú] [Botón] O colocarlos verticalmente: [Logo] [Menú] [Botón] dependiendo de las reglas utilizadas. 🟦 También existe CSS Grid Otra herramienta muy importante es: display: grid; CSS Grid permite construir diseños con filas y columnas. Por ejemplo: ┌───────────────┐ │ Header │ ├─────┬─────────┤ │Menu │ Content │ │ │ │ ├─────┴─────────┤ │ Footer │ └───────────────┘ Todo esto puede lograrse sin cambiar el contenido HTML. Solo cambiando cómo se distribuyen sus elementos. 📦 El modelo de caja Cada elemento HTML puede imaginarse como una caja. Esa caja tiene varias partes: Margin ↓ Border ↓ Padding ↓ Content En CSS esto se conoce como: Box Model Por ejemplo: .card { padding: 20px; margin: 10px; border: 1px solid #ccc; } Aquí estamos controlando: Espacio interno. Espacio externo. Borde. Entender este modelo es fundamental para posicionar correctamente elementos. 🎨 CSS también controla tipografías Podemos definir: h1 { font-family: Arial; font-size: 48px; font-weight: 700; } Y cambiar completamente la personalidad visual de una página. La misma estructura puede sentirse: Corporativa. Minimalista. Juvenil. Elegante. Técnica. Creativa. solamente modificando tipografía, colores y espaciado. ✨ También puede crear animaciones CSS no es únicamente diseño estático. También puede crear transiciones. Por ejemplo: button { transition: 0.3s; } button:hover { transform: scale(1.05); } Cuando el usuario coloca el cursor sobre el botón, este puede aumentar ligeramente. No fue necesario modificar el HTML. 🚀 Otro ejemplo: ocultar y mostrar elementos Supongamos que tenemos un menú: <nav class="menu"> ... </nav> Podemos ocultarlo: .menu { display: none; } Y mostrarlo bajo cierta condición: .menu.activo { display: block; } JavaScript solamente tendría que agregar o quitar la clase: activo CSS se encarga de la presentación. 🧠 ¿Qué significa la palabra "cascada"? La palabra Cascading es muy importante. En una página puede haber varias reglas intentando modificar el mismo elemento. Por ejemplo: p { color: blue; } Después: .descripcion { color: red; } Y quizá: #producto { color: green; } El navegador necesita decidir cuál debe utilizar. Para eso existen reglas relacionadas con: Especificidad. Orden. Herencia. Importancia. Eso es parte de la llamada cascada . 🥇 La especificidad Algunas reglas tienen más prioridad que otras. De forma simplificada: Elemento < Clase < ID Por ejemplo: p { color: blue; } y: .destacado { color: red; } Si un párrafo tiene la clase destacado , probablemente terminará siendo rojo porque esa regla es más específica. Esta prioridad permite combinar estilos generales con excepciones específicas. 🧬 La herencia Algunas propiedades pueden heredarse desde elementos padres. Por ejemplo: body { color: #333; } Muchos elementos dentro del body pueden utilizar ese color automáticamente. Conceptualmente: body │ ├── h1 ├── p └── div Ciertas propiedades pueden pasar hacia los hijos. Esto evita tener que repetir constantemente las mismas reglas. 🚀 Variables CSS CSS también permite definir valores reutilizables. Por ejemplo: :root { --color-principal: #af1716; } Después: button { background: var(--color-principal); } Y: h1 { color: var(--color-principal); } Si mañana queremos cambiar el color principal de todo el sitio: --color-principal: blue; podemos modificarlo en un solo lugar. Esto facilita muchísimo mantener diseños grandes. 🌙 Variables para temas También pueden utilizarse para crear temas. Por ejemplo: :root { --fondo: white; --texto: black; } Modo oscuro: .dark { --fondo: #111; --texto: white; } Después: body { background: var(--fondo); color: var(--texto); } Así gran parte de la página puede cambiar solamente modificando unas pocas variables. 🚀 Un ejemplo real: tienda en línea Supongamos que el HTML contiene: Producto Imagen Precio Descripción Botón comprar El diseño actual puede ser: ┌───────────────┐ │ Imagen │ │ Producto │ │ Precio │ │ Comprar │ └───────────────┘ Pero mañana el equipo de diseño decide cambiarlo: ┌───────┬───────────────┐ │Imagen │ Producto │ │ │ Precio │ │ │ Descripción │ │ │ [Comprar] │ └───────┴───────────────┘ Si la estructura está correctamente diseñada, gran parte de ese cambio puede realizarse mediante CSS. El contenido sigue siendo el mismo. Solo cambia su distribución. 🧩 Separar contenido y presentación facilita el mantenimiento Imagina que cada color estuviera definido directamente en cientos de etiquetas HTML. Cambiar el diseño completo sería complicado. En cambio: HTML ↓ Contenido CSS ↓ Diseño permite trabajar con responsabilidades separadas. Esto hace más sencillo: Cambiar temas. Mantener estilos. Reutilizar componentes. Adaptar diseños. Trabajar en equipo. ⚠️ Error común: abusar de estilos inline También podemos escribir estilos directamente en HTML: <p style="color:red;"> Hola </p> Esto funciona. Pero si hacemos eso constantemente podemos terminar mezclando demasiado estructura y presentación. Por ejemplo: <div style="display:flex; padding:20px; margin:10px; background:white;"> Cuando hay cientos de elementos así, mantener el diseño puede volverse complicado. Por eso normalmente conviene organizar los estilos de forma reutilizable. 🧩 Clases reutilizables En lugar de repetir: background: red; color: white; padding: 10px; border-radius: 5px; en muchos elementos, podríamos crear: .boton-principal { background: red; color: white; padding: 10px; border-radius: 5px; } Y reutilizarla. Eso ayuda a mantener consistencia. 🛠️ ¿Y frameworks como Tailwind o Bootstrap? Herramientas como: Tailwind CSS. Bootstrap. siguen trabajando sobre CSS. Simplemente ofrecen otra forma de organizar y reutilizar estilos. Por ejemplo, Bootstrap proporciona componentes y clases predefinidas. Tailwind ofrece clases de utilidad. Pero al final el navegador sigue procesando CSS. Estas herramientas no reemplazan el concepto. Lo organizan de una manera diferente. ⚠️ CSS también puede afectar el rendimiento Aunque parezca solamente visual, una hoja de estilos muy desorganizada también puede generar problemas. Por ejemplo: Archivos enormes. Reglas innecesarias. Animaciones pesadas. Selectores excesivamente complejos. Estilos duplicados. Además, ciertos cambios pueden provocar que el navegador tenga que recalcular posiciones o volver a dibujar partes de la pantalla. Por eso también conviene mantener los estilos organizados. 🎯 CSS no cambia el significado del contenido Aquí existe una diferencia importante. CSS puede hacer que un título sea: Grande Rojo Centrado Pero sigue siendo un título. HTML es quien define: <h1> Esto es importante también para aspectos como: Accesibilidad. SEO. Lectores de pantalla. Estructura semántica. Un buen diseño no debería utilizar CSS para reemplazar una estructura HTML correctamente construida. 🧠 HTML semántico + CSS Por ejemplo, es mejor utilizar: <button>Comprar</button> que: <div class="parece-boton"> Comprar </div> aunque CSS pueda hacer que ambos se vean exactamente iguales. Visualmente pueden parecer iguales. Pero semánticamente no lo son. La separación correcta sería: HTML ↓ Qué representa CSS ↓ Cómo se ve 📱 Mobile First Una estrategia muy utilizada consiste en diseñar primero pensando en pantallas pequeñas. Esto se conoce como: Mobile First Por ejemplo: .productos { grid-template-columns: 1fr; } Primero una columna. Después: @media (min-width: 768px) { .productos { grid-template-columns: repeat(2, 1fr); } } Y para pantallas mayores: @media (min-width: 1200px) { .productos { grid-template-columns: repeat(4, 1fr); } } La página crece progresivamente según el espacio disponible. 🚀 Una misma página puede adaptarse completamente Podemos terminar con: Celular ↓ 1 columna Menú compacto Texto pequeño Botones grandes Tablet: 2 columnas Menú diferente Computadora: 4 columnas Menú completo Más espacio Todo utilizando el mismo contenido HTML. ⚠️ Error común: pensar que CSS es solo "poner colores" Cuando alguien comienza desarrollo web puede pensar que CSS sirve simplemente para: Cambiar color Cambiar fuente Cambiar tamaño Pero CSS participa en prácticamente toda la construcción visual de una interfaz. Controla: Layout. Responsividad. Animaciones. Posicionamiento. Espaciado. Temas. Tipografías. Estados interactivos. Visibilidad. Adaptación entre dispositivos. Es una parte fundamental del frontend. 🛠️ Buenas prácticas ✔️ Mantén una estructura HTML semántica. ✔️ Separa contenido y presentación cuando sea posible. ✔️ Reutiliza estilos y evita duplicación innecesaria. ✔️ Utiliza variables para valores repetidos. ✔️ Diseña pensando en diferentes tamaños de pantalla. ✔️ Evita depender únicamente de tamaños fijos. ✔️ Organiza tus clases y estilos con una convención clara. ✔️ No utilices CSS para reemplazar elementos semánticos de HTML. ✔️ Prueba la interfaz en celulares, tablets y computadoras. ✔️ Mantén animaciones y efectos bajo control. 🧩 La realidad Una página web puede conservar exactamente la misma estructura y aun así cambiar completamente de apariencia. Puede pasar de: ☀️ Claro. a: 🌙 Oscuro. De: 💻 Escritorio. a: 📱 Celular. De: 📋 Diseño sencillo. a: ✨ Interfaz completamente distinta. sin necesidad de cambiar necesariamente el contenido. Eso es posible porque HTML y CSS tienen responsabilidades diferentes. HTML define qué existe. CSS decide cómo debe presentarse. 💬 Una interfaz no se vuelve diferente porque cambie su contenido. Muchas veces cambia simplemente porque CSS está interpretando y organizando ese mismo contenido de otra manera. 👉 La próxima vez que cambies entre modo claro y oscuro o abras una página desde otro dispositivo, recuerda que probablemente el HTML sea prácticamente el mismo. Lo que está cambiando es la forma en que CSS le dice al navegador que debe mostrarlo. 🔥 El backend no se ve, pero sin él, nada funciona.

14 ago 2026
Leer publicación
¿Por qué algunas aplicaciones responden en milisegundos aunque tengan millones de usuarios?
Base de datos

¿Por qué algunas aplicaciones responden en milisegundos aunque tengan millones de usuarios?

Abres una aplicación. Inicias sesión. Buscas un producto. Consultas tu perfil. Actualizas una página. Y todo ocurre casi al instante. Pero detrás puede haber millones de registros, miles de usuarios conectados y una gran cantidad de peticiones ocurriendo al mismo tiempo. Entonces surge una pregunta: 👉 ¿Cómo consiguen algunas aplicaciones responder tan rápido? Una de las estrategias más comunes consiste en utilizar una base de datos en memoria . 🧠 ¿Qué es una base de datos en memoria? Una base de datos en memoria guarda la información principalmente en la RAM . Esto permite acceder a los datos muchísimo más rápido que si hubiera que buscarlos constantemente en almacenamiento persistente. Podemos imaginarlo así: Base de datos tradicional ↓ Datos almacenados en disco ↓ Lectura ↓ Respuesta Mientras que una base en memoria trabaja de forma más cercana a esto: Aplicación ↓ RAM ↓ Dato ↓ Respuesta La RAM está diseñada para ofrecer acceso extremadamente rápido. Por eso este tipo de herramientas es muy útil cuando una aplicación necesita consultar constantemente la misma información. Uno de los ejemplos más conocidos es Redis . ⚡ RAM vs almacenamiento Para entender por qué esto importa, hay que recordar una diferencia básica. La memoria RAM es muchísimo más rápida que el almacenamiento persistente tradicional. Esto no significa que una base de datos tradicional sea lenta. Motores como PostgreSQL, MySQL o SQL Server están muy optimizados. Además, utilizan sus propios sistemas de caché. Pero cuando una aplicación necesita recuperar constantemente ciertos datos, evitar trabajo repetitivo puede generar una mejora importante. Por ejemplo: Usuario 1 → consultar categorías Usuario 2 → consultar categorías Usuario 3 → consultar categorías Usuario 4 → consultar categorías Usuario 5 → consultar categorías Si las categorías cambian muy poco, quizá no tenga sentido ejecutar una consulta completa contra la base principal para cada usuario. Podríamos guardar temporalmente el resultado en memoria. ⚙️ ¿Cómo funciona? Supongamos que tenemos un endpoint: GET /productos/destacados Cada vez que alguien entra a la página principal, la aplicación necesita obtener los productos destacados. Sin una capa en memoria: Usuario ↓ Backend ↓ Base de datos ↓ Consulta ↓ Resultado Esto ocurre para cada usuario. Ahora imagina miles de peticiones. La misma consulta puede repetirse constantemente. Con Redis o una herramienta similar podemos hacer: Usuario ↓ Backend ↓ ¿Dato está en Redis? ↓ Sí ↓ Devolver inmediatamente La base de datos principal ni siquiera participa en esa petición. 🔎 ¿Y si el dato todavía no está en memoria? Entonces normalmente ocurre algo como esto: Usuario solicita dato ↓ Buscar en Redis ↓ ❌ No existe ↓ Consultar base principal ↓ Obtener resultado ↓ Guardar resultado en Redis ↓ Responder La primera petición puede necesitar consultar la base principal. Pero las siguientes pueden obtener directamente el valor almacenado en memoria. Este patrón es muy común y suele conocerse como cache-aside . 🚀 Un ejemplo real: una tienda en línea Imagina una tienda con: 500,000 productos Miles de usuarios entran cada minuto. Muchos consultan exactamente la misma información: Categorías. Productos populares. Configuraciones. Promociones. Información del menú. Resultados precalculados. Si cada petición ejecutara repetidamente consultas sobre la base principal: SELECT ... SELECT ... SELECT ... SELECT ... podríamos aumentar innecesariamente la carga. En cambio: Redis ├── categorias ├── promociones ├── destacados └── configuracion permite servir rápidamente ciertos datos que se consultan con mucha frecuencia. La base principal queda disponible para operaciones más importantes. 📈 El beneficio no es solamente velocidad Utilizar memoria también puede disminuir muchísimo la carga sobre otros sistemas. Imagina que una consulta tarda: 20 ms No parece demasiado. Pero si ejecutas esa misma consulta: 10,000 veces por minuto la base de datos tiene que repetir el trabajo constantemente. Si el resultado cambia solamente una vez cada cinco minutos, podrías guardar una copia temporal. Ahora, en lugar de: 10,000 consultas SQL quizá tengas: 1 consulta SQL + 9,999 lecturas desde caché La diferencia puede ser enorme. 🧩 Redis no es solamente una caché Redis suele asociarse principalmente con caché. Pero puede utilizarse para mucho más. Por ejemplo: Sesiones. Contadores. Rate limiting. Colas. Locks distribuidos. Datos temporales. Rankings. Pub/Sub. Información de tiempo real. Esto es posible porque Redis ofrece diferentes estructuras de datos. No trabaja solamente con valores simples. Puede manejar estructuras como: Strings Hashes Lists Sets Sorted Sets Streams Esto lo hace muy útil para distintos problemas de backend. 👤 Ejemplo: sesiones de usuario Supongamos que tienes varios servidores: Backend 1 Backend 2 Backend 3 Los usuarios pueden llegar a cualquiera de ellos. Si guardas las sesiones solamente dentro de la memoria de cada servidor, aparece un problema. El usuario podría iniciar sesión en: Backend 1 pero la siguiente petición llegar a: Backend 2 y ese servidor no tendría información sobre su sesión. Una solución es almacenar las sesiones en un sistema compartido como Redis. Backend 1 ─┐ Backend 2 ─┼→ Redis Backend 3 ─┘ Así todos los servidores pueden consultar el mismo estado. 🔢 Otro ejemplo: contadores Redis también es muy útil para operaciones simples y frecuentes. Por ejemplo: Número de visitas Intentos de login Likes Solicitudes realizadas Imagina un contador: visitas = 1,500 Redis puede incrementar ese valor de forma muy rápida. Por ejemplo: INCR visitas Esto es muy útil cuando necesitas operaciones frecuentes y atómicas. 🚦 Rate limiting Otro uso común es limitar cuántas peticiones puede hacer un usuario. Por ejemplo: Máximo: 100 solicitudes por minuto Cada vez que llega una petición: usuario_25 → 34 solicitudes usuario_25 → 35 solicitudes usuario_25 → 36 solicitudes un sistema en memoria puede llevar estos contadores rápidamente. Además, el dato puede expirar automáticamente cuando termina la ventana de tiempo. Por eso Redis aparece frecuentemente en implementaciones de rate limiting. ⏳ La expiración es una de sus grandes ventajas Una característica muy útil es poder asignar un tiempo de vida a los datos. Por ejemplo: productos_destacados TTL = 300 segundos Esto significa que el valor puede mantenerse durante cinco minutos. Después se elimina automáticamente. La próxima petición: Cache miss ↓ Consultar base de datos ↓ Guardar nuevamente Esto permite evitar mantener datos viejos indefinidamente. 🧠 ¿Qué es TTL? TTL significa: Time To Live Representa cuánto tiempo debe permanecer disponible un valor antes de expirar. Por ejemplo: Clave: categorias TTL: 1 hora O: Clave: codigo_verificacion TTL: 5 minutos Cuando termina ese tiempo, el sistema puede eliminar automáticamente la información. Esto es ideal para datos temporales. ⚠️ El problema de los datos desactualizados Aquí aparece uno de los principales desafíos. Imagina que Redis contiene: precio_producto_10 = $500 Pero alguien cambia el precio en la base principal: precio_producto_10 = $450 Ahora tenemos: Base principal → $450 Redis → $500 La aplicación podría mostrar información antigua. Por eso utilizar caché también requiere pensar en invalidación . 🔄 ¿Cómo actualizar la caché? Existen distintas estrategias. Una opción es simplemente esperar a que expire. Por ejemplo: TTL = 5 minutos Durante unos minutos el dato podría permanecer antiguo. Después se vuelve a consultar. Esto puede ser aceptable para cierta información. Pero para otros datos no. 🗑️ Otra estrategia: invalidar al modificar Cuando actualizamos el producto: UPDATE productos SET precio = 450 también podemos eliminar la versión en caché: DELETE cache producto_10 Entonces la siguiente lectura hará: Cache miss ↓ Consultar BD ↓ Precio = 450 ↓ Guardar nuevamente Así reducimos la posibilidad de servir información obsoleta durante demasiado tiempo. 🚨 La invalidación de caché es complicada Una frase muy conocida en software dice que la invalidación de caché es uno de los problemas difíciles de la informática. Y tiene sentido. Guardar un dato es sencillo. El reto es saber: ¿Cuándo deja de ser válido? Por ejemplo: Producto cambió ↓ ¿Actualizar producto en caché? ↓ ¿También categorías? ↓ ¿También búsquedas? ↓ ¿También recomendaciones? Un solo cambio puede afectar varias claves almacenadas. Por eso una estrategia de caché mal diseñada puede mostrar información inconsistente. ⚠️ Redis no reemplaza automáticamente tu base principal Este es uno de los errores más comunes. Una arquitectura típica puede tener: Aplicación ↓ Redis ↓ PostgreSQL Pero eso no significa: Redis = reemplazo de PostgreSQL Cada herramienta puede cumplir una función distinta. La base principal puede encargarse de la información permanente y de relaciones complejas. Redis puede encargarse de datos temporales o de acceso frecuente. 💾 ¿Los datos en RAM desaparecen si el servidor se apaga? La RAM es volátil. Normalmente, cuando una máquina pierde energía, el contenido de la memoria desaparece. Sin embargo, herramientas como Redis pueden configurarse con mecanismos de persistencia. Por ejemplo, pueden escribir información en disco periódicamente o registrar operaciones. Esto permite recuperar parte o todo el estado después de un reinicio, dependiendo de la configuración. Pero sigue siendo importante entender qué garantías necesitas. No deberías asumir simplemente: "Está en Redis, entonces nunca puedo perderlo." 🧩 Caché y base en memoria no son exactamente lo mismo Aunque Redis se utiliza muchísimo como caché, técnicamente puede funcionar como una base de datos en memoria. La diferencia está en cómo lo uses. Por ejemplo: Redis como caché ↓ Dato principal vive en PostgreSQL Pero también existen casos donde Redis almacena información que forma parte directamente del funcionamiento del sistema. Por eso conviene separar los conceptos: Base de datos en memoria describe dónde y cómo se mantienen principalmente los datos. Caché describe el propósito de guardar una copia temporal para acelerar accesos. Muchas veces ambas ideas aparecen juntas. Pero no son exactamente sinónimos. 🔥 ¿Qué pasa si Redis deja de funcionar? Aquí aparece otra consideración importante. Si construyes toda tu aplicación asumiendo que Redis siempre estará disponible, puedes crear otro punto de fallo. Supongamos que solamente lo utilizas para caché. Si Redis falla, una estrategia posible sería: Redis no disponible ↓ Consultar base de datos directamente ↓ Aplicación sigue funcionando Tal vez más lenta. Pero disponible. En cambio, si utilizas Redis para sesiones, locks o datos críticos, su caída puede tener consecuencias distintas. Por eso también debe diseñarse pensando en disponibilidad. 📉 Cache miss y cache hit Cuando trabajas con caché aparecen dos conceptos importantes. Cache Hit El dato está disponible en memoria. Buscar producto ↓ Redis ↓ ✅ Existe ↓ Responder Esto es ideal. Cache Miss El dato no está. Buscar producto ↓ Redis ↓ ❌ No existe ↓ Consultar base principal Un buen sistema de caché busca tener un porcentaje alto de hits para la información donde realmente vale la pena almacenar resultados. 📊 La tasa de aciertos importa Imagina que implementas Redis pero tienes: Cache hit: 5% Cache miss: 95% Quizá esa estrategia no está aportando demasiado. En cambio: Cache hit: 90% podría representar una reducción importante de consultas. Esto demuestra algo importante: 👉 No basta con agregar Redis. Hay que medir si realmente está ayudando. ⚠️ Cache stampede Existe otro problema interesante. Imagina que una clave muy popular expira: productos_populares Y justo en ese momento llegan: 10,000 usuarios Todos hacen: Buscar en Redis ↓ No existe ↓ Consultar base de datos Ahora tienes 10,000 consultas intentando regenerar exactamente el mismo valor. Eso puede saturar la base principal. A este tipo de problema se le conoce como cache stampede . 🛠️ ¿Cómo puede evitarse? Existen diferentes estrategias. Por ejemplo: Locks temporales. Renovación anticipada. TTL con pequeñas variaciones. Regeneración en segundo plano. Permitir que un solo proceso reconstruya el valor. La idea es evitar que miles de solicitudes intenten reconstruir la misma información simultáneamente. 🧠 No todo debe almacenarse en caché Otro error común es pensar: "Si Redis es rápido, voy a meter absolutamente todo ahí." Eso puede ser contraproducente. La memoria RAM es un recurso limitado. Además, almacenar datos que casi nunca se consultan no genera demasiado beneficio. La caché funciona mejor cuando se utiliza para información: Frecuentemente consultada + Relativamente costosa de obtener + Con una estrategia clara de expiración 🔐 Tampoco olvides la seguridad La velocidad no elimina las responsabilidades de seguridad. Si almacenas datos sensibles en memoria, necesitas considerar: Acceso a Redis. Autenticación. Red. Cifrado cuando sea necesario. Separación de ambientes. Permisos. Un Redis expuesto incorrectamente puede convertirse en un problema serio. 🚀 Un flujo completo Imagina un endpoint: GET /productos/25 La aplicación podría seguir este proceso: Request ↓ Buscar producto:25 en Redis ↓ ¿Existe? ├── Sí │ ↓ │ Responder │ └── No ↓ Consultar PostgreSQL ↓ Guardar en Redis ↓ TTL = 10 minutos ↓ Responder Ahora, si llegan otras 1,000 solicitudes: Redis ↓ Respuesta rápida sin repetir la consulta principal. 🧩 ¿Cuándo conviene utilizar una base en memoria? Puede tener mucho sentido para: ✔️ Datos consultados repetidamente. ✔️ Sesiones. ✔️ Contadores. ✔️ Rate limiting. ✔️ Caché de respuestas. ✔️ Información temporal. ✔️ Rankings. ✔️ Locks distribuidos. ✔️ Colas o eventos, dependiendo del caso. Pero la decisión debería basarse en un problema real. No utilizar Redis simplemente porque "todas las aplicaciones grandes lo usan". ⚠️ Añadir Redis también agrega complejidad Antes tenías: Aplicación ↓ Base de datos Ahora tienes: Aplicación ├── Redis └── Base de datos Eso significa más cosas que monitorear. Ahora tienes que pensar en: Expiraciones. Memoria disponible. Inconsistencias. Reconexiones. Disponibilidad. Persistencia. Estrategias de fallback. Métricas. La mejora de rendimiento tiene un costo arquitectónico. Por eso debe existir una razón clara para agregar esa pieza. 🛠️ Buenas prácticas ✔️ Utiliza memoria para datos donde realmente exista beneficio. ✔️ Define TTL cuando la información sea temporal. ✔️ Diseña una estrategia clara de invalidación. ✔️ No uses Redis como sustituto automático de tu base principal. ✔️ Considera qué ocurre si Redis deja de estar disponible. ✔️ Monitorea cache hits y cache misses. ✔️ Evita almacenar información innecesaria. ✔️ Protege el acceso al servidor Redis. ✔️ Piensa en posibles cache stampedes. ✔️ Mide el rendimiento antes y después de implementar caché. 🧩 La realidad Una aplicación rápida no siempre tiene una base de datos extremadamente poderosa. Muchas veces simplemente evita repetir trabajo. En lugar de preguntar constantemente: Base de datos: "Dame otra vez la misma información." puede hacer: Redis: "Ya tengo ese resultado." Y responder inmediatamente. Las bases de datos en memoria permiten aprovechar la velocidad de la RAM para resolver problemas donde cada milisegundo importa. Pero no son una solución mágica. Necesitan una estrategia correcta de expiración, sincronización y disponibilidad. Cuando se utilizan bien, pueden reducir drásticamente la carga de una base principal y mejorar la experiencia de millones de usuarios. 💬 Un sistema rápido no siempre procesa más información. Muchas veces simplemente evita procesar la misma información una y otra vez . 👉 La próxima vez que una aplicación responda en milisegundos, recuerda que quizá la respuesta ni siquiera tuvo que llegar hasta su base de datos principal. 🔥 El backend no se ve, pero sin él, nada funciona.

13 ago 2026
Leer publicación