Herman Primo.

Herman Enrique Primo Escobar

|

Backend Developer | Desarrollo de APIs seguras, automatización y soluciones backend escalables | Python • PHP • NestJs • NextJs • React • MySQL • PostgreSQL • AWS • Git • Linux | Divulgando conocimiento para todos

Villahermosa, Tabasco, México
Herman Enrique Primo Escobar

Experiencia y Skills

Tecnología aplicada en proyectos reales.

Experiencia profesional, formación académica y habilidades técnicas conectadas con soluciones backend, APIs e infraestructura.

PrimoTec

Profesional independiente · Híbrido

Desarrollador full stack

jul 2024 - actualidad · 2 años 2 meses

Villahermosa, Tabasco, México

Desde 2024 he trabajado de manera independiente desarrollando solucion...

Desarrollo Full StackInfraestructura de softwarePythonDjango

Servicios de Mantenimiento y Soporte Técnico

sep 2018 - actualidad · 8 años

Villahermosa, Tabasco, México

Desde 2018, brindo servicios tecnológicos y soporte técnico especializ...

Soporte TécnicoInfraestructuraLinuxUbuntu Server

SITAI

Jornada completa · Híbrido

Responsable de TI

nov 2024 - feb 2026 · 1 año 3 meses

Ciudad del Carmen, Campeche, México

Me desempeñé como Responsable de TI y Desarrollador Full Stack en SITAI, participando tanto en el desarrollo de soluciones tecnológicas como en la administración y mantenimiento de la infraestructura tecnológica de la empresa. Trabajé principalmen...

Soporte TécnicoDirección de TIPythonDjango

TecNM | HackatecNM Nacional 2024

Contrato de prácticas · Presencial

Desarrollador de back-end

oct 2024 - nov 2024 · 1 mes

Colima, Colima, México

Participé como Backend Developer en la etapa nacional de HackaTec TecNM 2024, una competencia intensiva de innovación tecnológica desarrollada durante 48 horas continuas, donde colaboré en el desarrollo de una aplicación móvil enfocada en el monit...

Desarrollo web back endBases de datosPythonDjango

TecNM | HackatecNM Regional 2024

Contrato de prácticas · Presencial

Desarrollador de back-end

ago 2024 - sep 2024 · 1 mes

Villahermosa, Tabasco, México

Participé como Backend Developer en HackaTec TecNM 2024, una competencia regional de innovación tecnológica desarrollada durante 36 horas continuas, donde colaboré en el desarrollo de una aplicación enfocada en la atención rápida de emergencias sa...

Desarrollo web back endBases de datosGolangPostgreSQL

TecNM | Campus Villahermosa

Contrato de prácticas · Presencial

Practicante de Soporte y Sistemas

ago 2021 - ago 2024 · 3 años 1 mes

Villahermosa, Tabasco, México

Brindé apoyo técnico y operativo en el centro de cómputo institucional, participando en el desarrollo y actualización de sistemas internos, mantenimiento de plantillas y soporte a plataformas tecnológicas. Colaboré en la revisión y administración...

Soporte TécnicoDesarrollo de softwarePHPBootstrap

1/5

Skills

Backend

1/5

Backend

Python
90%
Django
90%
Django REST Framework
90%
PHP
80%
NestJS
70%
Golang
60%

Frontend

Next.js
85%
React
85%
JavaScript
90%
TypeScript
70%
Tailwind CSS
90%
Bootstrap
85%

Cloud

AWS EC2
75%
AWS RDS
70%

DevOps

Docker
80%
Nginx
80%
Linux
90%
Gunicorn
70%

Base de datos

MySQL
90%
PostgreSQL
85%

Herramientas

Git
90%
Postman
90%

Infraestructura

Ubuntu Server
80%
Linux Server Administration
80%

Mobile

Ionic
75%
Flutter
70%

Diseño UI/UX

Figma
70%
Responsive Design
85%

Testing

Selenium
75%
API Testing
85%

Sobre mí

Diseño la lógica que mantiene funcionando los sistemas.

Backend, APIs, automatización e infraestructura pensada para resolver problemas reales.

Backend Developer enfocado en el desarrollo de APIs seguras, automatización de procesos y soluciones escalables. Me apasiona construir sistemas eficientes, desde la lógica del servidor y la arquitectura backend hasta la integración con bases de datos y servicios en la nube.

He participado en proyectos académicos y tecnológicos relacionados con plataformas digitales, marketplaces y soluciones orientadas a innovación y tecnología. Además, disfruto compartir contenido técnico sobre backend, bases de datos, APIs y arquitectura de software. Actualmente continúo fortaleciendo mis conocimientos y desarrollando soluciones modernas y escalables.

Cliente

API

Backend

DB

Cloud

Paso 1

Cliente

Una persona entra a tu sitio, inicia sesión o realiza una acción.

Servicios

Soluciones tecnológicas para construir, integrar y escalar.

Servicios enfocados en backend, APIs, bases de datos, infraestructura cloud y mantenimiento de aplicaciones.

Proyectos

Soluciones construidas con enfoque real.

Una selección de proyectos donde aplico backend, APIs, bases de datos, infraestructura y desarrollo full stack.

Ver todos los proyectos
Portafolio Profesional
Base de datosAPI REST

Portafolio Profesional

Portafolio profesional desarrollado para centralizar experiencia, habilidades, proyectos, servicios y contenido técnico en una plataforma moderna y escalable.

Blog

Ideas, backend y tecnología explicados con claridad.

Publicaciones técnicas sobre backend, APIs, bases de datos, seguridad, arquitectura y desarrollo de software.

Ver todas las publicaciones
¿Por qué las computadoras empiezan a contar desde 0 y no desde 1?
Curiosidad de programación

¿Por qué las computadoras empiezan a contar desde 0 y no desde 1?

Carrusel: ¿Por qué tantos lenguajes empiezan a contar desde 0? Slide 1 — Hook 🤔 Tienes una lista con 5 elementos: ["A", "B", "C", "D", "E"] Pero sus posiciones son: 0 1 2 3 4 Espera... ¿5 elementos pero el último está en la posición 4? 😵‍💫 Si nosotros contamos: 1, 2, 3, 4, 5 👉 ¿por qué en tantos lenguajes de programación todo empieza desde 0 ? Sígueme para más contenido de Backend 🚀 💬 Comenta tu experiencia o dudas. @hermanprimodev Slide 2 — Deja de pensar en “número” En lugar de preguntar: ❌ “¿Qué número de elemento es?” prueba con: 📍 “¿Qué tan lejos está del inicio?” [A] [B] [C] [D] [E] ↑ INICIO Para llegar a A : ➡️ avanzas 0 posiciones. Para llegar a B : ➡️ avanzas 1 . Para llegar a C : ➡️ avanzas 2 . 💡 De repente, el cero empieza a tener sentido. Sígueme para más contenido de Backend 🚀 💬 Comenta tu experiencia o dudas. @hermanprimodev Slide 3 — Ahora llévalo a memoria Supongamos que: 📦 Cada elemento ocupa 4 bytes 📍 El arreglo comienza en 1000 Podemos pensar en: dirección = inicio + índice × tamaño Entonces: array[0] → 1000 + 0×4 = 1000 array[1] → 1000 + 1×4 = 1004 array[2] → 1000 + 2×4 = 1008 👉 El índice 0 apunta naturalmente al inicio porque su desplazamiento es exactamente cero. Sígueme para más contenido de Backend 🚀 💬 Comenta tu experiencia o dudas. @hermanprimodev Slide 4 — Pero hay una trampa Esto NO significa que: ❌ “Las computadoras siempre cuentan desde cero.” El indexado desde cero es una convención muy común. La encontramos en lenguajes como: C C++ Java JavaScript Python Pero no es una ley universal. Existen lenguajes, estructuras y APIs que utilizan otras bases de indexación. 👉 Lo importante es entender qué representa el índice en el contexto que estás usando. Sígueme para más contenido de Backend 🚀 💬 Comenta tu experiencia o dudas. @hermanprimodev Slide 5 — Y de aquí nace un clásico 😅 Tienes: 5 elementos Pero los índices son: 0 1 2 3 4 Por eso solemos recorrerlos con una condición como: i < longitud y no: i <= longitud Porque con longitud = 5 : array[4] ✅ array[5] ❌ Equivocarte por una posición puede producir el famoso: ⚠️ off-by-one error Un pequeño +1 o -1 ... puede ser suficiente para romper tu lógica. Sígueme para más contenido de Backend 🚀 💬 Comenta tu experiencia o dudas. @hermanprimodev Slide 6 — array[0] ya no parece tan raro Cuando veas: array[0] no pienses: 🤨 “Dame el elemento número cero.” Piensa: 📍 “Dame el elemento que está a CERO posiciones del inicio.” [A] [B] [C] [D] [E] 0 1 2 3 4 ↑ 0 posiciones Ese pequeño cambio de perspectiva explica por qué el indexado desde cero encaja tan bien con desplazamientos. 🧠 El índice no tiene que representar cuántos elementos llevas contando. También puede representar qué tan lejos estás del comienzo. 🔥 El backend no se ve, pero sin él, nada funciona. Sígueme para más contenido de Backend 🚀 💬 ¿Recuerdas la primera vez que array[0] te hizo pensar “¿por qué esto empieza en cero?” 😅 @hermanprimodev Elegí construir el carrusel alrededor de un cambio de perspectiva: de “número de elemento” a “distancia desde el inicio” . Primero hacemos que el 0 parezca extraño y después usamos exactamente ese mismo 0 para que la explicación encaje visualmente. Dejé fuera más detalles históricos y de implementación porque el ejemplo de direcciones de memoria ya comunica la intuición principal. La precisión importante es que empezar desde cero no es una obligación del hardware ni de todos los lenguajes; es una convención muy extendida y especialmente natural cuando un índice se interpreta como desplazamiento.

14 sep 2026
Leer publicación
¿Por qué borrar un archivo no significa que desaparezca inmediatamente del disco?
Curiosidad tecnológica

¿Por qué borrar un archivo no significa que desaparezca inmediatamente del disco?

🗑️ Borras una fotografía de tu computadora… pero eso no siempre significa que sus datos desaparezcan inmediatamente Tienes una fotografía llamada: vacaciones.jpg La eliminas. Después vacías la papelera. La buscas nuevamente. Ya no aparece. Desde la perspectiva del usuario parece bastante claro: “El archivo ya no existe.” Pero internamente la historia puede ser diferente. Dependiendo del sistema de archivos, del dispositivo de almacenamiento y de cómo se haya realizado la eliminación, los datos que formaban ese archivo podrían permanecer físicamente almacenados durante cierto tiempo. Entonces aparece una pregunta interesante: 👉 ¿Qué significa realmente “borrar” un archivo? 🧠 Primero: guardar un archivo implica mucho más que almacenar sus bytes Cuando guardas algo como: vacaciones.jpg el sistema no simplemente coloca la fotografía en algún lugar del disco y espera que luego puedas encontrarla. Necesita mantener información que describa ese archivo. Conceptualmente podríamos imaginar algo así: vacaciones.jpg ↓ metadatos ↓ ubicación de los datos ↓ almacenamiento El sistema de archivos puede mantener información como: Nombre del archivo. Tamaño. Fechas. Permisos. Ubicación o referencias hacia sus datos. Estado del espacio utilizado. La implementación exacta depende del sistema de archivos. NTFS, ext4, APFS y otros utilizan estructuras diferentes. Pero la idea general permanece: 👉 El sistema necesita una manera de saber qué datos pertenecen a cada archivo. 📚 El sistema de archivos funciona un poco como un índice Imagina tener un disco lleno de información sin ningún mecanismo para organizarla. Tendrías millones de bloques de datos, pero no sabrías: ¿Cuáles pertenecen a foto.jpg? ¿Cuáles pertenecen a documento.pdf? ¿Cuáles están libres? ¿Cuáles forman parte del sistema? El sistema de archivos proporciona esa organización. Simplificando muchísimo: Archivo ↓ Sistema de archivos ↓ Referencia a bloques ↓ Datos físicos Cuando abres una fotografía, el sistema utiliza esa información para localizar las partes correspondientes y presentarlas como un único archivo. Por eso eliminar un archivo no necesariamente necesita comenzar destruyendo cada uno de sus bits. Puede empezar simplemente modificando esas referencias. ⚙️ ¿Qué ocurre cuando presionas “Eliminar”? Muchas veces existe primero una etapa intermedia. En sistemas de escritorio, cuando envías un archivo a la papelera, normalmente el archivo todavía sigue existiendo. Simplemente fue movido o marcado de manera que aparezca dentro de la papelera. Por eso puedes restaurarlo fácilmente. El flujo podría verse así: vacaciones.jpg ↓ 🗑️ Papelera ↓ Restaurar En este punto todavía no hablamos necesariamente de una eliminación definitiva. Cuando vacías la papelera o utilizas una eliminación que la omite, entonces el sistema puede marcar el archivo como eliminado. Pero incluso ahí no significa necesariamente: sobrescribir todos los datos con ceros inmediatamente 🧩 Muchas veces borrar significa liberar espacio lógico Supongamos que un archivo ocupa determinados bloques. Antes: Bloque 100 → vacaciones.jpg Bloque 101 → vacaciones.jpg Bloque 102 → vacaciones.jpg Después de eliminarlo, el sistema podría pasar a considerar: Bloque 100 → disponible Bloque 101 → disponible Bloque 102 → disponible La diferencia importante es esta: ANTES espacio perteneciente al archivo DESPUÉS espacio disponible para reutilización No necesariamente hubo que destruir en ese instante el contenido anterior. El sistema simplemente deja de considerar esos bloques como parte de un archivo válido. 🚀 Una analogía sencilla: una biblioteca Imagina una biblioteca enorme. En su catálogo aparece: Libro A → Estante 47 Cuando alguien solicita el libro, el bibliotecario consulta el catálogo y sabe exactamente dónde encontrarlo. Ahora imagina que decides eliminarlo del inventario. Podrías hacer: Eliminar: Libro A → Estante 47 y marcar: Estante 47 → disponible Desde el punto de vista administrativo: Libro A ya no existe. Pero físicamente el libro podría continuar ahí. Simplemente nadie debería buscarlo utilizando el catálogo. Algún tiempo después alguien coloca: Libro B en ese mismo estante. Ahora el contenido anterior sí fue reemplazado. Borrar archivos puede parecerse conceptualmente a esto en determinados sistemas. 🔎 ¿Entonces por qué existen programas para recuperar archivos borrados? Precisamente porque a veces existe una diferencia entre: El archivo dejó de estar registrado. y: Los datos fueron físicamente reemplazados. Si todavía existe suficiente información en el almacenamiento, una herramienta de recuperación puede intentar encontrarla. Hay varias posibilidades. Podría recuperar información sobre el archivo que todavía permanezca en estructuras del sistema de archivos. O podría buscar directamente patrones conocidos dentro de los datos. Por ejemplo, determinados formatos tienen estructuras identificables. Una herramienta puede buscar firmas que indiquen: Aquí probablemente comienza un JPEG. y tratar de reconstruir el contenido. 🧩 Recuperar el nombre y recuperar los datos no son exactamente lo mismo Imagina que eliminaste: vacaciones-cancun-2026.jpg Puede ocurrir que los datos de la imagen todavía estén presentes, pero la información original sobre: nombre ruta fecha ya no esté completamente disponible. Una herramienta podría recuperar entonces algo como: file000237.jpg El contenido sobrevivió. Pero parte de los metadatos utilizados para identificarlo se perdió. Por eso una recuperación puede producir archivos aparentemente completos con nombres completamente diferentes. 🧱 ¿Qué pasa si el archivo estaba fragmentado? Otro problema aparece cuando un archivo no estaba almacenado de manera completamente contigua. Imagina: vacaciones.jpg parte 1 → bloque 100 parte 2 → bloque 500 parte 3 → bloque 900 El sistema de archivos sabe cómo relacionar esas partes. Pero después de una eliminación, algunas de esas referencias podrían perderse. Una herramienta de recuperación necesitaría reconstruir correctamente: parte 1 + parte 2 + parte 3 Si una de ellas ya fue sobrescrita: parte 1 ✅ parte 2 ❌ parte 3 ✅ el archivo podría recuperarse parcialmente o quedar corrupto. Por eso recuperar un archivo eliminado no siempre significa obtenerlo exactamente como estaba. ⚠️ Por eso debes dejar de utilizar la unidad si borraste algo importante Supongamos que eliminaste accidentalmente un documento. El sistema ahora considera sus bloques: disponibles Después instalas un programa. Descargas videos. Copias fotografías. Actualizas el sistema. Cada una de esas acciones puede generar escrituras. El almacenamiento podría reutilizar justamente esos bloques. Conceptualmente: Documento eliminado ↓ espacio disponible ↓ descargas archivo nuevo ↓ parte del espacio se reutiliza ↓ datos antiguos sobrescritos Cuanto más escribes, mayores pueden ser las posibilidades de reemplazar información que todavía era recuperable. Por eso una recomendación habitual ante una eliminación accidental importante es: 👉 minimizar nuevas escrituras sobre esa unidad. 💿 En un disco duro tradicional, este modelo es relativamente intuitivo En un HDD, la información se almacena magnéticamente sobre platos. Simplificando mucho, el sistema puede marcar determinados sectores como disponibles sin necesidad de sobrescribirlos inmediatamente. Mientras esos sectores no sean reutilizados, parte de la información anterior puede permanecer. Por eso históricamente ha sido posible recuperar archivos eliminados de discos duros incluso después de vaciar la papelera. Pero esto no significa que la recuperación esté garantizada. Depende de: Cuánto tiempo pasó. Cuánto se utilizó el disco. Qué partes fueron sobrescritas. Qué información del sistema de archivos sobrevivió. El estado físico del dispositivo. ⚡ En un SSD la historia cambia bastante Los SSD no funcionan como discos duros. No tienen platos magnéticos ni cabezales. Utilizan memoria flash y un controlador que administra internamente dónde colocar la información. Y ahí aparece un mecanismo importante: TRIM . 🧠 ¿Qué es TRIM? Cuando eliminas un archivo, el sistema operativo sabe que determinados bloques lógicos ya no contienen información que necesites conservar. Pero el SSD también necesita saberlo. TRIM permite comunicarle algo conceptualmente parecido a: Estos bloques ya no contienen información válida que necesite conservarse. El SSD puede utilizar esa información para preparar espacio para futuras escrituras. Este comportamiento puede mejorar el rendimiento y la gestión interna del dispositivo. Pero tiene una consecuencia importante: 👉 La recuperación de archivos eliminados puede resultar mucho más difícil. 🔄 Un SSD no necesariamente escribe donde tú imaginas Supongamos que desde el sistema operativo ves: Bloque lógico 100 No significa necesariamente que exista una relación simple y permanente con una única posición física de memoria. El controlador del SSD utiliza mecanismos internos para administrar las celdas. Puede hacer cosas como: Wear leveling. Garbage collection. Remapeo de bloques. Gestión de páginas. Movimiento interno de datos. Todo esto ocurre para utilizar mejor la memoria flash y prolongar su vida útil. Por eso pensar: “Voy a sobrescribir exactamente el mismo sector varias veces” no funciona en SSD de la misma manera intuitiva que podría imaginarse con un HDD. 🧹 ¿Qué es Garbage Collection en un SSD? La memoria flash tiene restricciones importantes. Generalmente no puede simplemente sobrescribir cualquier pequeño fragmento directamente de la misma manera que otros tipos de almacenamiento. Las operaciones de borrado se realizan en unidades mayores. Por eso el controlador necesita reorganizar internamente información válida y liberar bloques. Conceptualmente: Bloque ├── Datos válidos ├── Datos eliminados ├── Datos eliminados └── Datos válidos El SSD puede copiar los datos todavía válidos a otro lugar y liberar el bloque completo. Este proceso se relaciona con el garbage collection . Si los datos eliminados son tratados como innecesarios, eventualmente pueden dejar de existir físicamente sin que tú escribas directamente sobre ellos. ⚠️ Esto explica por qué recuperar desde SSD puede ser mucho más complicado En un HDD tradicional podrías tener: archivo eliminado ↓ sectores no sobrescritos ↓ posible recuperación En un SSD con TRIM activo podría ocurrir: archivo eliminado ↓ TRIM ↓ SSD sabe que esos datos ya no son necesarios ↓ garbage collection ↓ datos eliminados internamente El tiempo exacto y el comportamiento dependen del dispositivo, sistema operativo y configuración. Por eso no deberías asumir: “Como acabo de eliminarlo, todavía estará físicamente ahí.” Con almacenamiento moderno, eso puede dejar de ser cierto mucho antes de lo esperado. 🔐 El cifrado cambia todavía más el problema Imagina un dispositivo cuyo almacenamiento está completamente cifrado. Los datos físicos podrían existir como bloques cifrados. Para interpretarlos necesitas una clave. Eso permite otra estrategia interesante de eliminación segura: 👉 destruir las claves necesarias para descifrar los datos. Esto suele conocerse como crypto erase o borrado criptográfico. Conceptualmente: Datos cifrados + Clave = Información legible Si destruyes correctamente la clave: Datos cifrados + ❌ clave = información prácticamente inutilizable Esto puede ser mucho más rápido que intentar sobrescribir físicamente enormes cantidades de almacenamiento. 🛡️ Por eso el cifrado completo del disco es importante Imagina que alguien roba una computadora. Si el disco no está cifrado, dependiendo del sistema y de cómo se hayan eliminado archivos, podría existir información recuperable. Con cifrado completo correctamente configurado, el atacante primero necesita superar la protección criptográfica. Herramientas como: BitLocker FileVault LUKS son ejemplos de tecnologías utilizadas para cifrar almacenamiento en diferentes sistemas. El cifrado no sustituye buenas políticas de seguridad, pero cambia radicalmente el riesgo asociado a datos residuales. 🗑️ “Eliminar” y “eliminar de forma segura” son operaciones diferentes Cuando utilizas el botón normal: Eliminar el objetivo principal suele ser: dejar de mostrar el archivo + recuperar su espacio Pero cuando necesitas eliminar información sensible, el objetivo cambia: hacer que recuperar los datos sea prácticamente inviable Son problemas diferentes. Por eso sistemas y dispositivos pueden ofrecer funciones especiales de borrado seguro. 🧹 ¿Sobrescribir muchas veces un archivo garantiza destruirlo? Existe una idea bastante popular: “Para eliminar algo de forma segura debes sobrescribirlo siete, diez o treinta veces.” En discos modernos esto suele ser una simplificación innecesaria. En discos magnéticos actuales, una sola sobrescritura adecuada suele ser suficiente para escenarios normales. Y en SSD existe un problema adicional: 👉 El controlador puede redirigir escrituras hacia celdas diferentes. Por ejemplo, tú podrías intentar: sobrescribir bloque lógico 100 pero internamente el SSD podría colocar esos nuevos datos en una ubicación física diferente por razones de wear leveling. Eso significa que una herramienta que intenta sobrescribir un archivo individual no necesariamente controla qué ocurre físicamente con cada copia anterior de esos datos. 🧰 ¿Qué usar entonces para borrar un SSD de forma segura? Depende del dispositivo, sistema operativo y nivel de seguridad requerido. Para eliminar completamente una unidad pueden existir opciones como: Secure Erase proporcionado por el dispositivo. Sanitize commands. Funciones del fabricante. Borrado criptográfico. Restablecimientos seguros. Herramientas específicas del sistema. No existe un comando universal que sea correcto para todos los SSD. En entornos donde la información es especialmente sensible, también pueden existir procedimientos de destrucción física. La estrategia debe corresponder al riesgo. ☁️ ¿Y qué ocurre con archivos almacenados en la nube? La situación se vuelve todavía más interesante. Cuando borras algo de un servicio en la nube, puede existir: Archivo visible ↓ Papelera ↓ Eliminación lógica ↓ Sistemas internos ↓ Replicación ↓ Backups No significa necesariamente que todos los bytes desaparezcan instantáneamente de cada dispositivo físico del proveedor. Los servicios suelen tener políticas de retención, replicación, respaldos y eliminación. Por eso debes distinguir entre: Ya no accesible para el usuario y: eliminado definitivamente de toda infraestructura El segundo proceso puede depender de políticas internas y periodos de retención. 📱 También ocurre en teléfonos Cuando borras una fotografía en un smartphone muchas veces sucede primero: Foto ↓ Eliminados recientemente ↓ 30 días ↓ eliminación Mientras se encuentra en esa sección, recuperarla puede ser trivial. Después de eliminarla definitivamente, la posibilidad de recuperación dependerá del almacenamiento, cifrado, TRIM y comportamiento interno del dispositivo. Los smartphones modernos utilizan almacenamiento flash, por lo que las ideas de un viejo disco duro no siempre se aplican directamente. 🧩 El sistema de archivos también puede conservar información adicional Algunos sistemas utilizan mecanismos como: Journaling. Snapshots. Versiones. Copias de seguridad. Esto significa que eliminar un archivo de su ubicación principal no siempre implica que ninguna otra copia exista. Por ejemplo, podrías borrar: /documentos/reporte.pdf pero conservar una versión dentro de: snapshot de ayer Esto es muy útil para recuperación. Pero también importa cuando tu objetivo es destruir información sensible. Eliminar la copia principal no necesariamente elimina todas las versiones históricas. 📸 ¿Por qué a veces una recuperación encuentra miniaturas aunque no encuentre la fotografía? Muchos sistemas y aplicaciones generan archivos derivados. Por ejemplo: foto-original.jpg ↓ miniatura ↓ preview ↓ cache Puedes eliminar el archivo original pero continuar teniendo una miniatura guardada en otra ubicación. Eso significa que una herramienta forense podría encontrar: preview pequeño ✅ archivo original ❌ La información puede haber dejado rastros en diferentes lugares. Por eso la eliminación segura de información sensible puede ser bastante más compleja que borrar un único archivo. 🔍 Recuperación normal vs análisis forense También conviene separar dos conceptos. Una herramienta doméstica de recuperación puede buscar archivos borrados relativamente recientes. Un análisis forense profesional puede intentar reconstruir mucha más información utilizando: Estructuras del sistema de archivos. Fragmentos. Metadatos. Cachés. Snapshots. Logs. Copias temporales. Eso no significa que todo archivo eliminado sea recuperable. Pero sí demuestra que: 👉 “No aparece en el explorador” no equivale automáticamente a “no queda ningún rastro”. ⚠️ Error común: instalar el programa de recuperación en el mismo disco Imagina que eliminaste accidentalmente: tesis.docx y enseguida descargas un programa de recuperación de 500 MB al mismo disco. Acabas de generar nuevas escrituras. Incluso la instalación puede crear: archivos cachés logs temporales y alguno de ellos puede reutilizar espacio donde estaba la tesis. Cuando el archivo es realmente importante, lo ideal suele ser reducir al mínimo cualquier escritura sobre la unidad afectada y trabajar desde otra unidad o entorno cuando sea posible. ⚠️ Tampoco conviene ejecutar reparaciones sin entender qué hacen Si una unidad parece dañada y contiene información importante, ejecutar inmediatamente herramientas que modifiquen el sistema de archivos puede cambiar estructuras necesarias para recuperación. En casos críticos: datos importantes + fallo físico o corrupción puede ser preferible trabajar primero sobre una copia o imagen del dispositivo. Especialmente si el disco hace ruidos, desaparece o presenta fallos físicos, seguir utilizándolo puede empeorar la situación. 🛠️ Buenas prácticas si borraste accidentalmente algo importante De forma general: ✔️ Deja de utilizar la unidad tanto como sea posible. ✔️ Evita copiar archivos nuevos allí. ✔️ No instales herramientas de recuperación en esa misma unidad. ✔️ Comprueba primero papelera, backups, snapshots y servicios de nube. ✔️ Si el dispositivo está fallando físicamente, evita pruebas destructivas. ✔️ Si los datos son realmente críticos, considera recuperación profesional. Y, sobre todo: 👉 No dependas de la recuperación como estrategia de respaldo. 💾 La mejor recuperación sigue siendo tener un backup Los programas de recuperación existen porque las cosas salen mal. Pero confiar en ellos es apostar a que los datos no hayan sido sobrescritos o eliminados definitivamente. Un backup evita esa incertidumbre. Conceptualmente: Archivo original ↓ eliminación accidental ❌ Backup ↓ restaurar ✅ Para archivos importantes, es mucho más seguro mantener copias independientes que esperar que una herramienta pueda reconstruir información eliminada. 🔐 Y si vas a vender o regalar un dispositivo, borrar tus carpetas no es suficiente Imagina que vas a vender una laptop. Haces: Seleccionar todo ↓ Eliminar ↓ Vaciar papelera Eso no debería considerarse automáticamente una sanitización segura del dispositivo. Una estrategia adecuada depende del almacenamiento y del sistema. En equipos modernos suele ser recomendable utilizar las funciones oficiales de restablecimiento seguro, borrado del dispositivo o herramientas proporcionadas para ese hardware, especialmente si la unidad estaba cifrada. El objetivo debería ser asegurarte de que el siguiente propietario no pueda acceder razonablemente a tu información anterior. 🧠 Hay tres niveles que conviene distinguir Podemos resumir el concepto con tres estados diferentes. 1. Archivo visible vacaciones.jpg ✅ El sistema sabe dónde está y puedes abrirlo. 2. Archivo eliminado vacaciones.jpg ❌ espacio reutilizable ✅ Ya no aparece normalmente, pero parte de sus datos podría continuar presente. 3. Datos destruidos o inaccesibles datos sobrescritos o bloques eliminados o clave criptográfica destruida En este punto recuperar la información puede ser inviable. La transición entre el segundo y el tercer estado depende mucho del tipo de almacenamiento. 🧩 La realidad Cuando presionas: Eliminar no necesariamente estás ordenando: “Encuentra cada bit de este archivo y destrúyelo físicamente ahora mismo.” Muchas veces estás diciendo algo mucho más parecido a: Este archivo ya no debe formar parte del sistema de archivos. El espacio que ocupaba puede reutilizarse. Después, lo que ocurra físicamente depende de múltiples capas: Sistema operativo ↓ Sistema de archivos ↓ Controlador ↓ Tipo de almacenamiento ↓ HDD / SSD / flash En un HDD, los datos pueden permanecer hasta que otros los sobrescriban. En un SSD, mecanismos como TRIM y garbage collection pueden hacer que desaparezcan de otra manera. Con cifrado, incluso puede ser suficiente destruir una clave para volver los datos inutilizables. Y si existen snapshots, backups o cachés, podrían sobrevivir copias adicionales. Por eso “borrar” no es una única operación universal. 💬 En almacenamiento, “ya no puedo verlo” y “los datos dejaron de existir” son dos afirmaciones completamente diferentes. 👉 ¿Alguna vez eliminaste accidentalmente una fotografía o documento y lograste recuperarlo después de vaciar la papelera? 🔥 El backend no se ve, pero sin él, nada funciona.

13 sep 2026
Leer publicación
Git merge vs rebase: hacen algo parecido, pero pueden cambiar por completo el historial de tu proyecto
Herramientas Developer

Git merge vs rebase: hacen algo parecido, pero pueden cambiar por completo el historial de tu proyecto

🌿 Tienes una rama con varios días de trabajo y necesitas integrarla al proyecto Tu compañero te dice: Haz merge. Otro desarrollador responde: Mejor haz rebase. Los dos caminos pueden terminar llevando tus cambios hacia la rama principal. Pero no hacen exactamente lo mismo. Y, sobre todo, no dejan la misma historia. Entonces aparece una pregunta bastante común cuando empiezas a trabajar seriamente con Git: 👉 ¿Qué diferencia realmente a git merge de git rebase ? 🧠 Partamos de dos ramas Imagina este historial: A — B — C ← main \ D — E ← feature La rama feature nació desde B . Mientras trabajabas en ella, main recibió el commit C . Ahora quieres integrar ambos trabajos. Tienes al menos dos estrategias muy conocidas: git merge o git rebase El resultado final puede contener cambios equivalentes, pero la forma en que Git registra la historia será distinta. 🔀 Opción 1: git merge Con merge , Git conserva ambas líneas de desarrollo y crea un punto donde se unen. Podrías hacer: git switch main git merge feature Y terminar con algo parecido a: A — B — C —— M \ / D — E M es un merge commit. Ese commit tiene dos padres: C E y representa explícitamente el momento en que ambas ramas se combinaron. Git no necesita fingir que la rama feature nunca se separó. Al contrario. La historia muestra claramente: main avanzó por un lado feature avanzó por otro y después se integraron 📖 Merge conserva la historia tal como ocurrió Esta es una de las mayores características de merge . Los commits: D E siguen siendo exactamente los mismos commits. No cambian sus identificadores. La historia conserva la bifurcación original. Conceptualmente: B ├── C └── D — E ↓ merge Esto puede ser útil cuando quieres entender: Cuándo se creó una rama. Qué commits pertenecían a una funcionalidad. En qué momento se integró. Qué líneas de desarrollo existieron en paralelo. El historial puede ser más ramificado, pero también más fiel a lo ocurrido. ⚡ ¿Siempre se crea un merge commit? No. Git también puede hacer un fast-forward merge . Imagina este caso: A — B ← main \ C — D ← feature Si main no avanzó desde que creaste feature , no hay dos historias que realmente necesiten fusionarse. Git puede simplemente mover el puntero de main : A — B — C — D ↑ main Eso se conoce como fast-forward . Puedes incluso forzar la creación de un merge commit usando opciones como: git merge --no-ff feature si el equipo quiere conservar explícitamente el hecho de que existió una rama. 🧹 Opción 2: git rebase rebase funciona de otra manera. En lugar de unir dos historias conservando la bifurcación, toma los commits de tu rama y los vuelve a aplicar sobre otra base. Partimos de: A — B — C ← main \ D — E ← feature Desde feature podrías ejecutar: git rebase main Git conceptualmente hace algo parecido a: 1. Identifica D y E 2. Los separa temporalmente 3. Mueve feature encima de C 4. Reaplica los cambios de D 5. Reaplica los cambios de E El resultado sería: A — B — C — D' — E' Ahora parece que el trabajo de feature comenzó después de C . El historial queda lineal. ⚠️ D' y E' no son los mismos commits Este punto es fundamental. Aunque: D' pueda contener prácticamente los mismos cambios que: D Git los considera commits diferentes. ¿Por qué? Porque el identificador de un commit depende de información como: Su contenido. Su padre. Sus metadatos. El árbol asociado. Antes: D → padre B Después del rebase: D' → padre C Cambió el padre. Por lo tanto cambia también el hash. Lo mismo ocurre con los commits siguientes. Por eso decimos que rebase : 👉 reescribe la historia . 🧬 ¿Por qué el hash cambia? En Git un commit no es simplemente: "estos archivos cambiaron" También forma parte de una cadena histórica. Podemos imaginarlo así: Commit E ↓ apunta a D ↓ apunta a B Si cambias la base: E' ↓ apunta a D' ↓ apunta a C ya no estás hablando de la misma cadena de commits. Aunque el código final sea equivalente, la historia es diferente. 🔀 Merge une historias; Rebase mueve historia Una forma sencilla de recordarlo es: MERGE Conserva: A — B — C \ D — E y las une. Mientras que: REBASE Toma: D — E y los vuelve a colocar después de: C Por eso ambos sirven para integrar trabajo, pero lo hacen con filosofías distintas. ⚔️ Entonces… ¿cuál es mejor? No existe una respuesta universal. Depende del flujo de trabajo del equipo. merge Tiene ventajas como: ✔️ Conserva commits existentes. ✔️ No reescribe historia. ✔️ Es más seguro con ramas compartidas. ✔️ Muestra claramente cuándo se integraron líneas de desarrollo. Pero también puede producir un historial más complejo: * Merge feature-x |\ | * commit 3 | * commit 2 | * commit 1 * | |/ * otro commit Si existen muchas ramas y muchos merges, leer git log puede ser más difícil. 🧹 Rebase puede hacer la historia más sencilla de leer Después de rebase puedes obtener: A — B — C — D' — E' En lugar de: A — B — C —— M \ / D — E Esto puede facilitar cosas como: git log --oneline porque los commits aparecen en una secuencia más lineal. También puede hacer más sencillo revisar cómo evolucionó una funcionalidad. Pero ese beneficio tiene un precio: 👉 estás creando nuevos commits. ⚠️ La regla más importante de Rebase Hay una regla práctica muy conocida: Evita hacer rebase de commits públicos que otras personas ya están utilizando. ¿Por qué? Supongamos que publicas: D — E Tu compañero descarga esos commits. Ahora tú haces: git rebase main y terminas con: D' — E' Tu historial ahora dice: A — B — C — D' — E' Pero tu compañero todavía tiene: A — B \ D — E Desde la perspectiva de Git: D != D' E != E' Aunque representen trabajo parecido. Ahora existen dos versiones distintas de la misma historia. 💥 ¿Qué ocurre cuando intentas hacer push? Después de un rebase, es común que un push normal sea rechazado. Por ejemplo: git push puede fallar porque el historial remoto contiene: D — E mientras tú ahora tienes: D' — E' Git no puede simplemente avanzar la rama. Para reemplazar el historial remoto tendrías que hacer un push forzado. Muchas veces se recomienda: git push --force-with-lease en lugar de: git push --force porque --force-with-lease intenta protegerte de sobrescribir cambios remotos que no esperabas. Aun así, sigue siendo una operación que debe utilizarse con cuidado. 🚨 ¿Por qué git push --force puede ser peligroso? Imagina que tu rama remota tiene: D — E — F Tu compañero agregó F . Pero tú solamente conoces: D — E Después haces rebase: D' — E' y ejecutas: git push --force Podrías terminar reemplazando: D — E — F por: D' — E' El commit F desaparece de la referencia remota. Por eso --force-with-lease suele ser una opción más prudente. Antes de sobrescribir, comprueba que el remoto siga en el estado que esperabas. 🏡 Rebase suele ser mucho más cómodo en ramas privadas Supongamos que creaste: feature/login y solamente tú estás trabajando allí. Puedes tener commits como: WIP fix otro fix cambio rápido Mientras tanto main avanza. Antes de abrir tu Pull Request puedes hacer: git fetch origin git rebase origin/main Ahora tu trabajo queda encima de los cambios más recientes. Si nadie más depende de esos commits, reescribirlos es mucho menos problemático. Por eso una regla práctica común es: Historia local o privada → rebase puede ser cómodo. Historia pública y compartida → mucho más cuidado. No es una ley absoluta, pero ayuda bastante. 🔄 ¿Qué sucede cuando hay conflictos? Tanto merge como rebase pueden producir conflictos. Supongamos que main cambió: const timeout = 5000; Mientras tu rama cambió la misma línea: const timeout = 10000; Git no puede decidir automáticamente qué versión debería sobrevivir. Con merge , podría detenerse y pedirte resolver el conflicto antes de crear el merge commit. Con rebase , Git puede detenerse mientras reaplica uno de tus commits. El flujo típico sería: git rebase main Git detecta conflicto. Lo resuelves. Después: git add archivo.js git rebase --continue Si quieres cancelar todo: git rebase --abort y vuelves al estado anterior al rebase. 🧩 Una diferencia importante al resolver conflictos Con un merge normalmente resuelves el conflicto en el punto de integración. Conceptualmente: C \ M ← resolver diferencias / E Con rebase, Git reaplica tus commits uno por uno. Si tienes: D E F G podrías encontrar conflictos durante varios de esos commits. Eso puede ser útil porque Git te obliga a resolver la historia paso a paso. Pero también puede resultar más laborioso en ramas grandes. 🧠 Rebase también puede ser interactivo Existe una herramienta muy poderosa: git rebase -i o: git rebase --interactive Por ejemplo: git rebase -i HEAD~4 Git puede mostrar: pick a1b2c3 Crear componente pick d4e5f6 Corregir estilo pick g7h8i9 Fix pick j1k2l3 Otro fix Y puedes decidir: pick reword edit squash fixup drop Esto permite reorganizar commits antes de compartirlos. 🧹 ¿Para qué sirve squash ? Supongamos que tienes: Agregar login Corregir login Fix login Ahora sí funciona login Antes de integrar podrías convertir todo en: Agregar autenticación de usuarios Con rebase interactivo: pick A Agregar login squash B Corregir login squash C Fix login squash D Ahora sí funciona login Git combina esos commits. Esto puede producir una historia más fácil de entender. Pero nuevamente: 👉 estás reescribiendo commits. Por eso suele hacerse antes de que otras personas comiencen a depender de ellos. 🧾 ¿Qué es un historial “limpio”? Decir: “Rebase deja un historial limpio” puede ser demasiado subjetivo. Para algunos equipos, esto: A — B — C — D — E — F es limpio porque es lineal. Para otros equipos: A — B —— M \ / C-D es más informativo porque muestra cómo se desarrollaron las funcionalidades. La limpieza depende de qué información quieres conservar. Un historial extremadamente lineal puede ser fácil de leer. Un historial con merges puede explicar mejor la estructura real del trabajo. 🧭 ¿Qué estrategia utilizan los equipos? Hay muchísimas. Un equipo podría trabajar así: feature branch ↓ commits locales ↓ rebase sobre main ↓ Pull Request ↓ merge Otro: feature branch ↓ Pull Request ↓ merge commit Otro puede usar: squash merge y convertir todo el Pull Request en un único commit. Otro puede utilizar: rebase and merge desde plataformas como GitHub. No existe un único flujo correcto. Lo importante es comprender qué historia produce cada opción. 🔀 ¿Qué es Squash Merge? Muchas plataformas permiten integrar una rama tomando todos sus cambios y convirtiéndolos en un único commit. Supongamos: feature: D — E — F Al hacer squash merge podrías terminar en main con: A — B — C — S donde S contiene los cambios combinados de: D + E + F Esto produce un historial principal muy compacto. Puede ser útil cuando los commits internos de una Pull Request son solamente pasos temporales de desarrollo. 🧠 Merge commit vs Squash Merge vs Rebase Merge Podemos imaginar tres resultados distintos. Merge commit A — B — C —— M \ / D — E Conserva la estructura de la rama. Squash merge A — B — C — S Combina los cambios de la rama en un único commit. Rebase and merge A — B — C — D' — E' Reaplica cada commit linealmente sobre main . Los tres pueden producir un árbol de archivos final parecido. Pero la historia queda diferente. 🛠️ Una estrategia práctica para una rama personal Imagina que estás trabajando solo en: feature/productos Mientras desarrollas haces: Ajustar modelo Agregar endpoint Fix endpoint Agregar validación Fix validación Mientras tanto main recibe nuevos cambios. Podrías hacer: git fetch origin git rebase origin/main Resolver cualquier conflicto. Luego revisar: git log --oneline Y, si quieres, utilizar: git rebase -i para mejorar tus commits antes de abrir el Pull Request. Este uso de rebase suele ser bastante cómodo porque todavía controlas completamente esa historia. ⚠️ Una estrategia menos recomendable Imagina una rama: develop utilizada activamente por 15 desarrolladores. Todos hacen: git pull y crean trabajo desde ella. Hacer rebase de toda esa rama y forzar el historial remoto puede provocar bastante caos. Cada desarrollador tendrá referencias hacia los commits antiguos. Después tendrán que reconciliar sus ramas con los nuevos commits. No significa que sea imposible. Significa que el costo y el riesgo son mucho mayores. 🧠 ¿Y qué hace git pull ? Este punto también suele causar confusión. Cuando ejecutas: git pull Git normalmente combina dos operaciones: git fetch + integración Según tu configuración, esa integración puede realizarse mediante merge o rebase. Por ejemplo: git pull --rebase obtiene los cambios remotos y vuelve a aplicar tus commits locales encima. Supongamos: Remoto: A — B — C Local: A — B — D Con pull --rebase podrías terminar: A — B — C — D' en vez de crear un merge commit local. ⚙️ Puedes configurar el comportamiento de git pull Si un equipo prefiere rebase: git config pull.rebase true Si prefiere merge: git config pull.rebase false También existen otras configuraciones. Lo importante es saber qué está haciendo Git. Ejecutar comandos sin entender si están haciendo merge o rebase es una fuente común de historiales inesperados. 🧩 ¿Qué ocurre con git bisect y el debugging? El historial no sirve solamente para verse bonito. También es una herramienta de diagnóstico. Comandos como: git bisect permiten buscar qué commit introdujo un problema. Un historial con commits pequeños y coherentes puede facilitar mucho esa tarea. Por ejemplo: A ✅ B ✅ C ✅ D ❌ E ❌ Git puede ayudarte a identificar: D como el posible commit problemático. Por eso, ya sea que uses merge o rebase, vale la pena pensar en la calidad de los commits. Un historial lineal lleno de: fix fix2 ahora sí final final-final no necesariamente es mejor que un historial con ramas bien organizadas. 🔍 ¿Qué es más fácil de revisar? Depende. Un historial lineal permite leer: Commit 1 ↓ Commit 2 ↓ Commit 3 ↓ Commit 4 sin saltos. Pero los merge commits pueden aportar contexto: Aquí se integró feature/pagos Aquí se integró feature/reportes Aquí se integró hotfix/login Algunos equipos prefieren usar merges para representar unidades de trabajo. Otros prefieren que main sea completamente lineal. Ambas decisiones pueden ser razonables. ⚠️ Error común: usar rebase solamente porque “se ve más profesional” Un historial bonito no compensa un equipo confundido. Antes de usar rebase deberías entender: ¿Qué commits voy a reescribir? ¿Ya están publicados? ¿Alguien más depende de ellos? ¿Tendré que hacer force push? ¿Estoy preparado para resolver conflictos? Si no conoces las respuestas, quizá merge sea una opción más segura. El objetivo no es utilizar el comando más sofisticado. Es conservar una historia comprensible sin poner en riesgo el trabajo compartido. ⚠️ Error común: pensar que Merge nunca genera problemas Merge tampoco es perfecto. Si creas constantemente commits como: Merge branch 'main' into feature Merge main Merge main otra vez Merge main nuevamente puedes terminar con un historial extremadamente ruidoso. Por eso algunos equipos prefieren actualizar ramas con rebase antes de abrir un Pull Request. Otros aceptan esos merges porque priorizan no reescribir historia. La estrategia depende de las convenciones. 🛠️ Una guía sencilla Si estás trabajando en una rama solamente tuya y quieres actualizarla con los últimos cambios: git fetch origin git rebase origin/main puede ser una opción cómoda. Si estás integrando una rama compartida y quieres preservar su historia: git merge puede ser más apropiado. Si necesitas convertir una Pull Request con muchos commits temporales en una sola unidad lógica: Squash merge puede resultar útil. Pero siempre revisa primero las reglas del proyecto. 🧪 Un ejemplo completo Partimos de: A — B — C ← main \ D — E ← feature Con merge Ejecutas: git switch main git merge feature Resultado: A — B — C —— M \ / D — E Los commits originales sobreviven. Con rebase Desde feature : git rebase main Resultado: A — B — C — D' — E' Después puedes integrar mediante fast-forward: git switch main git merge feature Resultado final: A — B — C — D' — E' El código puede terminar prácticamente igual. La historia no. 🧩 La realidad git merge y git rebase no son simplemente dos comandos diferentes para hacer “lo mismo”. Ambos pueden integrar líneas de desarrollo, pero responden a preguntas distintas. merge dice: Estas ramas existieron por separado y aquí fue donde se unieron. rebase dice: Voy a tomar estos commits y volverlos a aplicar sobre una base más reciente. Uno conserva la topología original. El otro reorganiza parte de la historia. Y por eso elegir entre ambos no consiste únicamente en conseguir que el código compile. También estás decidiendo: 👉 ¿Cómo quieres que Git recuerde la evolución de tu proyecto? Un historial lineal puede ser muy cómodo. Un historial con merges puede conservar información valiosa. Lo importante es que el equipo entienda la estrategia y la aplique de forma consistente. 💬 Un historial limpio puede ayudar muchísimo, pero entender qué commits estás modificando es mucho más importante que hacer que el gráfico se vea bonito. 👉 En tus proyectos, ¿prefieres conservar explícitamente las ramas con merge o mantener main más lineal utilizando rebase ? 🔥 El backend no se ve, pero sin él, nada funciona.

12 sep 2026
Leer publicación
¿Qué pasa si dos servidores intentan modificar el mismo dato al mismo tiempo?
Arquitectura de Software

¿Qué pasa si dos servidores intentan modificar el mismo dato al mismo tiempo?

🚨 Dos servidores leen exactamente el mismo dato… y uno de los cambios desaparece Imagina una aplicación con varias instancias del backend atendiendo peticiones al mismo tiempo. Dos usuarios realizan una operación casi simultáneamente. El estado actual es: stock = 10 Servidor A lee: stock = 10 Servidor B también lee: stock = 10 Ambos necesitan descontar una unidad. Servidor A calcula: 10 - 1 = 9 Servidor B calcula exactamente lo mismo: 10 - 1 = 9 Después ambos guardan: stock = 9 Pero ocurrieron dos ventas. El resultado correcto debería ser: stock = 8 👉 ¿Dónde desapareció una de las modificaciones? Bienvenido a uno de los problemas clásicos de concurrencia: la actualización perdida o lost update . 🧠 El problema no es tener varios servidores Tener múltiples instancias del backend es completamente normal. De hecho, es una estrategia muy común para distribuir tráfico y escalar una aplicación. Podríamos tener algo así: ┌── Backend A Usuario ──────┼── Backend B └── Backend C ↓ Base de datos El problema aparece cuando dos o más procesos intentan modificar el mismo estado compartido sin suficiente coordinación. Cada servidor puede estar funcionando perfectamente. Cada petición puede tener una lógica aparentemente correcta. Pero cuando las operaciones ocurren al mismo tiempo, empiezan los problemas. ⚙️ ¿Cómo ocurre una actualización perdida? El error suele aparecer cuando hacemos una secuencia como esta: 📖 Leer valor ↓ 🧮 Calcular nuevo valor ↓ 💾 Guardar resultado Con una sola petición no hay problema. Por ejemplo: stock = 10 ↓ 10 - 1 ↓ stock = 9 Pero con dos peticiones concurrentes puede ocurrir esto: Tiempo Servidor A Servidor B T1 lee stock = 10 T2 lee stock = 10 T3 calcula 9 T4 calcula 9 T5 guarda 9 T6 guarda 9 Ambas operaciones parten del mismo estado. La segunda escritura no sabe que la primera ya modificó el registro. El resultado final termina siendo: stock = 9 cuando debería ser: stock = 8 Una de las modificaciones se perdió. 🧮 El problema está en el famoso “leer → modificar → escribir” Este patrón aparece muchísimo en aplicaciones. Por ejemplo: producto = obtener_producto(id) producto.stock = producto.stock - 1 guardar(producto) A primera vista parece completamente correcto. Pero existen tres pasos distintos: 1. Leer 2. Calcular 3. Guardar Entre el paso 1 y el paso 3 otra petición puede modificar exactamente el mismo registro. Y ahí aparece la condición de carrera. 🏁 ¿Qué es una condición de carrera? Una race condition ocurre cuando el resultado depende del orden o del momento exacto en que varias operaciones concurrentes se ejecutan. Por ejemplo: Petición A ────────┐ ├── mismo dato Petición B ────────┘ Si A termina antes que B, obtienes un resultado. Si B termina antes que A, quizá obtengas otro. Y si ambas leen el mismo estado antes de escribir, puedes obtener un resultado incorrecto. Lo peligroso es que muchas veces el error no aparece durante desarrollo. Con una sola persona probando manualmente: click espera click espera todo funciona. En producción: Usuario 1 ─┐ Usuario 2 ─┤ Usuario 3 ─┤→ mismo recurso Usuario 4 ─┘ la historia cambia por completo. 🚀 No ocurre solamente con inventarios El stock es un ejemplo fácil de visualizar, pero el problema puede aparecer en muchos lugares. 💰 Saldos Imagina: saldo = 1000 Dos operaciones retiran 100 al mismo tiempo. Ambas leen: 1000 Ambas calculan: 900 Y ambas guardan: 900 Pero deberían quedar: 800 En un sistema financiero esto sería un error gravísimo. 🎟️ Disponibilidad Supongamos que queda: 1 asiento Dos usuarios intentan comprarlo casi al mismo tiempo. Si ambos leen: disponible = true antes de que uno de ellos termine la compra, podrías terminar vendiendo el mismo asiento dos veces. 📅 Reservas Un calendario muestra un horario libre: 10:00 - 11:00 Dos usuarios realizan la reserva simultáneamente. Si solamente haces: consultar disponibilidad ↓ crear reserva sin proteger la operación completa, ambos podrían superar la primera validación. 📊 Contadores Algo aparentemente inocente como: visitas = visitas + 1 también puede sufrir el mismo problema si varias peticiones realizan el cálculo fuera de una operación segura. 📦 Estados de pedidos Dos procesos podrían modificar un pedido simultáneamente. Por ejemplo: Proceso A: Pendiente → Pagado Proceso B: Pendiente → Cancelado Si ambos parten del mismo estado anterior, necesitas definir qué transición tiene prioridad y cuáles son válidas. La concurrencia no solamente puede perder números. También puede producir estados de negocio contradictorios. ✏️ También puede ocurrir cuando dos personas editan el mismo registro Imagina un sistema administrativo. Dos empleados abren al mismo cliente. La información inicial es: nombre: Ana estado: Pendiente telefono: 5551234 Empleado A cambia: estado: Aprobado Mientras tanto, empleado B modifica: telefono: 5559999 pero su formulario todavía contiene: estado: Pendiente Si B envía todo el registro: { "nombre": "Ana", "estado": "Pendiente", "telefono": "5559999" } puede sobrescribir accidentalmente el cambio de A. Resultado: telefono actualizado ✅ estado volvió a Pendiente ❌ Este tipo de conflicto también es una forma muy común de actualización perdida. 🛠️ ¿Cómo evitamos una actualización perdida? No existe una única estrategia correcta. Depende de cosas como: Qué tan crítica es la información. Cuánta concurrencia existe. Cuánto tiempo dura la operación. Si los conflictos son frecuentes. Qué experiencia quieres para el usuario. Pero hay varias herramientas importantes. ⚡ Opción 1: operaciones atómicas Volvamos al inventario. En lugar de hacer: leer stock ↓ stock - 1 ↓ guardar podemos pedirle directamente a la base de datos: UPDATE producto SET stock = stock - 1 WHERE id = 123; Aquí la operación se expresa directamente sobre el valor existente en la base de datos. No estamos haciendo: backend lee 10 backend calcula 9 backend envía 9 Estamos diciendo: Base de datos: "Resta 1 al valor actual." Eso cambia muchísimo el problema. La base de datos puede ejecutar esa modificación de forma atómica. 🧱 ¿Qué significa “atómica”? Una operación atómica se comporta conceptualmente como una unidad indivisible. Desde el punto de vista de otras operaciones, no existe un estado intermedio parcialmente aplicado. Por ejemplo: UPDATE contador SET valor = valor + 1 WHERE id = 1; Si varias peticiones ejecutan esta sentencia, la base de datos puede coordinar correctamente las modificaciones al mismo registro. Esto suele ser mucho más seguro que: valor = obtener_contador() valor += 1 guardar(valor) cuando existe concurrencia. ✅ Incluso puedes hacer la validación dentro de la misma operación Supongamos que no quieres permitir stock negativo. En lugar de: 1. consultar stock 2. comprobar stock > 0 3. descontar podrías usar una operación condicional: UPDATE producto SET stock = stock - 1 WHERE id = 123 AND stock > 0; Después puedes comprobar cuántas filas fueron modificadas. Si fue: 1 fila la reserva de stock funcionó. Si fue: 0 filas quizá ya no había disponibilidad. Esto reduce la ventana donde otra operación puede interponerse entre la comprobación y la modificación. 🔒 Opción 2: bloqueos Otra estrategia consiste en bloquear temporalmente el recurso mientras una operación crítica está trabajando con él. Por ejemplo: Transacción A ↓ Bloquea producto 123 ↓ Lee stock ↓ Modifica stock ↓ Confirma transacción ↓ Libera bloqueo Mientras tanto: Transacción B ↓ Intenta acceder al mismo registro ↓ ⏳ espera Dependiendo de la base de datos y del tipo de bloqueo utilizado, puedes impedir que operaciones conflictivas modifiquen simultáneamente el mismo dato. 🧠 Pessimistic Locking Este enfoque suele llamarse control de concurrencia pesimista . La idea es: “Existe una posibilidad suficiente de conflicto, así que voy a bloquear el recurso antes de modificarlo.” Conceptualmente: 🔒 bloquear ↓ 📖 leer ↓ 🧮 calcular ↓ 💾 modificar ↓ ✅ confirmar ↓ 🔓 liberar En SQL, dependiendo del motor, puede aparecer algo como: SELECT stock FROM producto WHERE id = 123 FOR UPDATE; normalmente dentro de una transacción. La sintaxis y el comportamiento exacto dependen del sistema de base de datos. ⚠️ Los bloqueos también tienen un costo Bloquear todo indiscriminadamente tampoco es una buena estrategia. Si mantienes bloqueos durante demasiado tiempo: Petición A 🔒 bloquea registro ↓ hace trabajo lento... ↓ llama API externa... ↓ procesa archivo... ↓ 10 segundos después 🔓 libera otras operaciones pueden quedarse esperando innecesariamente. Eso puede aumentar: Latencia. Contención. Uso de conexiones. Riesgo de deadlocks . Dificultad para escalar. Por eso las secciones críticas deberían ser lo más pequeñas posible. 🔄 Opción 3: control de concurrencia optimista Existe otra estrategia muy útil cuando los conflictos no ocurren constantemente. En lugar de bloquear el registro desde el principio, permitimos que varios usuarios lo lean. Pero antes de guardar comprobamos que nadie haya modificado el dato mientras trabajábamos. Por ejemplo: producto stock = 10 version = 7 El usuario carga esa versión. Después intenta actualizar diciendo: "Modifica este registro, pero solamente si sigue en version = 7." La consulta podría ser conceptualmente: UPDATE producto SET stock = 9, version = 8 WHERE id = 123 AND version = 7; Si alguien más ya actualizó el registro: version = 8 entonces la condición: version = 7 ya no coincide. Resultado: 0 registros actualizados Y acabas de detectar un conflicto. 🚦 ¿Qué hacer cuando detectas el conflicto? Detectarlo es solamente la mitad del problema. Después debes decidir qué hacer. Dependiendo de la operación puedes: ✔️ Reintentar automáticamente. ✔️ Volver a leer el estado actual. ✔️ Mostrar un mensaje al usuario. ✔️ Recalcular la operación. ✔️ Rechazar el cambio. ✔️ Pedir al usuario que revise las modificaciones. Por ejemplo, en un editor administrativo podrías mostrar: ⚠️ Este registro fue modificado por otro usuario. Tu versión: Pendiente Versión actual: Aprobado y permitir decidir cómo resolverlo. 🧠 Optimista vs pesimista Podemos resumirlos así: PESIMISTA "Creo que puede haber conflicto." ↓ Bloqueo antes de modificar OPTIMISTA "Probablemente no habrá conflicto, pero comprobaré antes de guardar." ↓ Detectar conflicto al actualizar Ninguno es universalmente mejor. Por ejemplo: Un asiento de avión muy demandado puede necesitar una estrategia diferente a la edición ocasional del nombre de un cliente. La elección depende del tipo de recurso y de las reglas del negocio. 🧾 También puedes utilizar timestamps o ETags La versión numérica: version = 7 es fácil de entender. Pero no es la única opción. En algunos sistemas puedes utilizar: updated_at o mecanismos como ETag . Por ejemplo, una API puede entregar: ETag: "registro-v7" Y el cliente intenta actualizar enviando: If-Match: "registro-v7" Si la versión actual ya cambió, el servidor puede rechazar la modificación. Esto permite aplicar control de concurrencia también a nivel HTTP. ⚠️ Error común: “Estoy usando transacciones, entonces ya estoy protegido” No necesariamente. Una transacción es una herramienta fundamental, pero el resultado depende de qué operaciones incluye y del nivel de aislamiento. Por ejemplo, podrías tener: Transacción A 📖 lee stock = 10 Transacción B 📖 lee stock = 10 Dependiendo del motor, del nivel de aislamiento y de cómo realizas las actualizaciones, ambos procesos todavía podrían trabajar inicialmente con información que entra en conflicto. Las transacciones permiten agrupar operaciones y proporcionar garantías importantes. Pero no convierten automáticamente cualquier lógica en segura frente a toda condición de carrera. 🧩 El nivel de aislamiento también importa Las bases de datos relacionales suelen ofrecer diferentes niveles de aislamiento. Conceptualmente, cuanto mayor es el aislamiento, más intenta comportarse el sistema como si las transacciones no estuvieran interfiriendo entre sí. Pero mayores garantías también pueden implicar más coordinación. Por eso existen diferentes niveles, como: Read Committed Repeatable Read Serializable Los nombres exactos son conocidos en SQL, aunque el comportamiento concreto puede variar entre motores. Serializable , por ejemplo, intenta ofrecer garantías muy fuertes, pero no significa que debas activarlo para absolutamente todo sin analizar costos y necesidades. 🔥 Serializable no elimina la necesidad de diseñar el comportamiento Incluso con garantías fuertes, una aplicación debe estar preparada para situaciones como: Transacción no puede completarse ↓ conflicto detectado ↓ rollback ↓ reintentar La concurrencia no siempre se resuelve consiguiendo que todas las operaciones “pasen”. A veces la solución correcta consiste precisamente en detectar que dos operaciones son incompatibles y hacer que una de ellas falle de forma controlada. 🛒 Ejemplo práctico: comprar la última unidad Supongamos: stock = 1 Dos usuarios presionan “Comprar”. Podemos usar una operación condicional: UPDATE producto SET stock = stock - 1 WHERE id = 123 AND stock > 0; La primera petición podría actualizar: 1 fila y dejar: stock = 0 La segunda intenta exactamente lo mismo. Pero ahora: stock > 0 ya no se cumple. Entonces actualiza: 0 filas El backend puede responder: 409 Conflict o una respuesta apropiada según el diseño de la API: { "error": "Producto sin disponibilidad" } No evitamos que las dos personas intentaran comprar. Coordinamos correctamente qué ocurre cuando compiten por el mismo recurso. 💳 Pero cuidado con operaciones de negocio más complejas Ahora supongamos que comprar implica: 1. Reservar stock 2. Crear pedido 3. Procesar pago 4. Confirmar compra Ya no estamos modificando solamente una columna. Si bloqueas stock durante toda una llamada a un proveedor de pagos que tarda varios segundos, quizá estés introduciendo otros problemas. Muchas aplicaciones separan conceptos como: stock disponible stock reservado stock vendido y utilizan expiraciones o procesos de compensación. Por ejemplo: Usuario inicia compra ↓ Reserva unidad durante 10 minutos ↓ Pago exitoso ↓ Reserva → venta Si el pago falla o expira: Reserva ↓ se libera ↓ stock vuelve a estar disponible La solución de concurrencia debe seguir las reglas reales del negocio. 📅 Otro ejemplo: reservar una cita Imagina una tabla: citas doctor_id fecha hora Podrías validar desde el backend: ¿Existe cita a las 10:00? ↓ No ↓ INSERT Pero dos peticiones podrían ejecutar la comprobación simultáneamente. Una protección adicional muy poderosa puede ser una restricción en la propia base de datos. Por ejemplo, conceptualmente: UNIQUE (doctor_id, fecha, hora) Ahora, aunque ambas peticiones intenten crear la misma reserva, la base de datos impide que existan dos registros incompatibles. Esto muestra otra idea importante: 👉 No toda protección de concurrencia tiene que implementarse únicamente con código del backend. 🧱 Las restricciones de base de datos también son parte de la defensa Las reglas críticas deberían protegerse lo más cerca posible del dato cuando sea razonable. Por ejemplo: UNIQUE CHECK FOREIGN KEY NOT NULL pueden impedir determinados estados inválidos. Supongamos que la regla es: “Un asiento solamente puede pertenecer a una reserva activa.” Si esa regla puede expresarse mediante restricciones o un diseño de datos adecuado, tienes una capa adicional de protección. No sustituye toda la lógica del negocio. Pero ayuda a evitar que un bug de aplicación termine dejando datos imposibles. 🌐 ¿Y qué cambia cuando tengo varios servidores? Curiosamente, muchas de estas técnicas funcionan precisamente porque la coordinación ocurre en un lugar compartido. Por ejemplo: Backend A ─┐ ├──→ Base de datos Backend B ─┘ Si ambos utilizan correctamente: Operaciones atómicas. Transacciones. Bloqueos. Versiones. Restricciones. no importa tanto cuál instancia recibió la petición. La base de datos funciona como punto de coordinación para ese estado. Eso es mucho más seguro que intentar mantener una variable local como: lock = True dentro de un único proceso. ⚠️ Un lock en memoria puede dejar de funcionar cuando escalas Supongamos que haces: Backend A 🔒 lock local Eso puede impedir que dos threads dentro de Backend A modifiquen simultáneamente algo. Pero ahora tienes: Backend A 🔒 lock A Backend B 🔒 lock B Cada servidor tiene su propio lock. Backend B no sabe que Backend A bloqueó algo. Entonces un mecanismo que funcionaba con una sola instancia deja de resolver el problema cuando escalas horizontalmente. La coordinación debe ocurrir en un sistema compartido si quieres proteger operaciones entre procesos diferentes. 🧠 No todos los datos necesitan la misma consistencia Aquí aparece una distinción importante. Perder una actualización en: contador aproximado de vistas quizá no tenga el mismo impacto que perder una actualización en: saldo bancario o: último asiento disponible Por eso no tiene sentido aplicar siempre el mecanismo más pesado posible a cada dato. Debes preguntarte: ¿Qué pasa si dos operaciones coinciden? ¿Puedo reintentar? ¿Puedo tolerar pequeñas diferencias? ¿Debo impedir absolutamente el conflicto? ¿Existe una única respuesta válida? La respuesta determina cuánto control necesitas. 🕒 Otro problema importante: operaciones que tardan demasiado Cuanto mayor sea el tiempo entre: leer y: escribir más grande es la ventana en la que puede aparecer un conflicto. Por ejemplo: Usuario abre formulario ↓ lee version = 7 ↓ lo deja abierto 25 minutos ↓ otra persona modifica el registro ↓ primer usuario finalmente guarda Aquí no estamos hablando de milisegundos. El mismo problema puede aparecer después de minutos. Por eso el control optimista resulta especialmente útil en interfaces donde las personas editan datos durante bastante tiempo. 🔁 ¿Debemos reintentar siempre? No. Reintentar puede ser útil para operaciones técnicas donde volver a ejecutar es seguro. Pero en algunos casos un reintento automático puede cambiar el significado del negocio. Supongamos que: stock = 1 Dos personas compran. Una gana. La otra recibe conflicto. Reintentar no puede fabricar una segunda unidad. En cambio, una operación como incrementar un contador puede ser más adecuada para un reintento automático. El sistema necesita distinguir entre: Conflicto temporal solucionable y: Regla de negocio que ya no puede cumplirse 🧩 ¿Qué pasa con sistemas todavía más distribuidos? Hasta ahora hablamos principalmente de varios backends compartiendo una base de datos. Pero los sistemas pueden ser mucho más complejos: Servicio A ↓ Servicio B ↓ Servicio C ↓ varias bases de datos En ese escenario coordinar cambios simultáneos puede ser todavía más difícil. No siempre existe una única transacción que abarque todo. Pueden aparecer estrategias como: Reservas. Compensaciones. Procesamiento asíncrono. Máquinas de estados. Detección de versiones. Operaciones diseñadas para poder repetirse de forma segura. Pero eso ya depende enormemente de la arquitectura. La idea fundamental sigue siendo la misma: 👉 Cuando varios actores modifican estado compartido, necesitas definir explícitamente cómo se resuelven los conflictos. ⚠️ Errores comunes al diseñar concurrencia Uno muy frecuente es asumir: “La posibilidad de que dos usuarios hagan clic exactamente al mismo tiempo es muy pequeña.” No necesitan hacerlo en el mismo microsegundo. Solamente necesitan coincidir dentro de la ventana entre leer y escribir. Otro error es pensar: “Como tengo una base de datos seria, ella resolverá cualquier problema automáticamente.” La base de datos proporciona herramientas. La aplicación debe utilizarlas correctamente. También es común: Mantener bloqueos demasiado tiempo. No comprobar cuántas filas modificó un UPDATE . Sobrescribir registros completos innecesariamente. No definir restricciones críticas en la base de datos. Reintentar operaciones que no deberían repetirse. Probar únicamente con una petición a la vez. 🧪 ¿Cómo pruebas este tipo de errores? La concurrencia necesita pruebas diferentes a las habituales. No basta con: hacer petición ↓ comprobar resultado Puedes ejecutar muchas operaciones simultáneas. Por ejemplo: stock inicial = 100 Lanzas: 100 peticiones concurrentes cada una descontando una unidad. Esperas: stock final = 0 Si terminas con: stock = 37 tienes un problema bastante evidente. Este tipo de pruebas puede revelar bugs que nunca aparecen realizando peticiones manuales una detrás de otra. 🛠️ ¿Qué estrategia debería utilizar? Una guía conceptual podría ser: ¿La modificación puede expresarse como una operación atómica? ↓ Sí ↓ Hazlo directamente en la base de datos. Si necesitas leer información y tomar varias decisiones: ¿Los conflictos son frecuentes y necesito exclusividad? ↓ Control pesimista / bloqueo Si los conflictos son poco frecuentes: Permite trabajo concurrente ↓ Comprueba versión al guardar ↓ Control optimista Y para reglas críticas: Añade también restricciones en la base de datos cuando sea posible. No es una receta universal, pero ayuda a pensar correctamente sobre el problema. 🧩 La realidad Cuando una aplicación crece, ya no puedes asumir que una sola petición estará trabajando con un dato a la vez. Puedes tener: 🖥️ Múltiples servidores ⚙️ Múltiples procesos 👥 Miles de usuarios 🔄 Operaciones simultáneas Y cada proceso puede ejecutar código perfectamente válido desde su propia perspectiva. El problema aparece cuando todos comparten el mismo estado. Ese es uno de los grandes desafíos de la concurrencia: Código correcto de forma aislada + ejecución simultánea = resultado potencialmente incorrecto Por eso diseñar un backend no consiste solamente en preguntarse: “¿Esta operación funciona?” También debes preguntarte: 👉 ¿Sigue funcionando si otra operación intenta modificar exactamente lo mismo al mismo tiempo? En inventarios, reservas, saldos, contadores y estados importantes, esa pregunta puede ser la diferencia entre un sistema que parece funcionar y uno que realmente mantiene sus datos correctos bajo carga. 💬 En sistemas concurrentes, el problema no siempre es que una petición falle. A veces ambas funcionan aparentemente bien y justamente por eso el dato termina mal. 👉 Si dos usuarios modificaran ahora mismo el mismo registro de tu aplicación, ¿tu sistema detectaría el conflicto o simplemente ganaría la última escritura? 🔥 El backend no se ve, pero sin él, nada funciona.

11 sep 2026
Leer publicación
¿Por qué una página puede verse rápido aunque todavía no esté lista para usarse?
Frontend

¿Por qué una página puede verse rápido aunque todavía no esté lista para usarse?

⚡ Entras a una página y parece cargar casi instantáneamente Ya puedes ver: 🖼️ Imágenes. 📝 Textos. 🔘 Botones. 📋 Formularios. Todo parece estar listo. Intentas presionar un botón y... Nada. Un segundo después vuelves a intentarlo. Ahora sí funciona. Esa sensación puede parecer extraña porque, desde tu perspectiva, la página ya estaba ahí. Entonces surge una pregunta importante: 👉 ¿Cómo puede una página estar visible pero todavía no estar completamente lista para usarse? Una posible explicación está en un proceso muy común en aplicaciones web modernas: Hydration . 🧠 ¿Qué es Hydration? En algunas arquitecturas web, el servidor puede enviar al navegador HTML que ya contiene parte de la interfaz renderizada. Eso significa que el navegador no siempre necesita esperar a que JavaScript construya absolutamente todo desde cero para comenzar a mostrar contenido. Puede recibir algo parecido a esto: <h1>Producto</h1> <p>$499</p> <button>Agregar al carrito</button> El navegador sabe cómo dibujar ese HTML inmediatamente. Por eso el usuario puede empezar a ver la página relativamente pronto. Pero existe un detalle: Ese HTML puede representar solamente la parte visual inicial. La lógica interactiva todavía puede necesitar JavaScript. Ahí entra la hidratación. De forma simplificada: Servidor genera HTML ↓ Navegador recibe el documento ↓ 👀 El usuario ve contenido ↓ JavaScript se descarga ↓ JavaScript se interpreta y ejecuta ↓ Framework conecta la lógica ↓ ✅ La interfaz queda interactiva La página puede verse antes de estar completamente preparada para responder a todas las acciones del usuario. 💧 ¿Por qué se llama “Hydration”? El nombre puede sonar un poco extraño. La idea es imaginar que el servidor envía una estructura que ya existe visualmente, pero que todavía necesita recuperar la lógica y el estado necesarios para funcionar como una aplicación interactiva. Por ejemplo, el servidor podría enviar: <button>Comprar</button> El navegador puede mostrar ese botón perfectamente. Pero el HTML por sí solo no sabe necesariamente qué debe ocurrir cuando haces clic. La lógica podría existir en JavaScript: function comprar() { agregarAlCarrito(); } Durante la hidratación, el framework relaciona la interfaz que ya existe en el documento con la aplicación que se está ejecutando en el navegador. Conceptualmente: HTML visible + Código JavaScript + Estado de la aplicación ↓ Interfaz interactiva Es como si la estructura ya estuviera construida y después se conectaran los mecanismos que permiten utilizarla. ⚙️ ¿Qué ocurre realmente en el navegador? Imagina una aplicación creada con un framework que soporta renderizado desde el servidor. Cuando visitas una página pueden ocurrir varias cosas. Primero, el navegador solicita el documento: GET /productos/123 El servidor responde con HTML. Ese HTML ya puede incluir: Nombre del producto Precio Imagen Descripción Botón de compra El navegador comienza a procesarlo y mostrarlo. Hasta aquí, el usuario ya puede pensar: “La página terminó de cargar.” Pero paralelamente todavía puede faltar descargar archivos JavaScript: /app.js /product-page.js /cart.js Después el navegador necesita: 📥 Descargar esos archivos. 🧩 Analizarlos. ⚙️ Ejecutarlos. 🧠 Reconstruir la información que necesita el framework. 🔗 Relacionar componentes con el HTML ya existente. Y solamente entonces determinadas interacciones quedan completamente disponibles. 🚀 Un ejemplo práctico: una tienda online Imagina que visitas una página de producto. El servidor genera inicialmente: 📸 Imagen del producto 📦 Nombre 💰 Precio ⭐ Calificación 🔘 Botón "Agregar al carrito" El navegador recibe ese HTML y puede mostrarlo casi inmediatamente. La interfaz parece lista. Pero el botón depende de código JavaScript que debe ejecutar algo parecido a: async function agregarAlCarrito(productoId) { await fetch("/api/carrito", { method: "POST", body: JSON.stringify({ producto_id: productoId }) }); } Mientras ese código todavía no está preparado, el botón puede existir visualmente sin tener todavía toda su funcionalidad conectada. Entonces existen dos momentos distintos: Momento 1 👀 Puedo ver la página. Momento 2 🖱️ Puedo interactuar correctamente con ella. Y esos momentos no necesariamente ocurren al mismo tiempo. 🔘 HTML visible no significa JavaScript listo Este es uno de los puntos más importantes para entender Hydration. El navegador sabe interpretar HTML sin necesidad de frameworks como React. Si recibe: <button>Guardar</button> puede mostrar un botón. Pero mostrarlo no significa que exista automáticamente una función asociada a él. Algo similar ocurre con: 📋 Formularios complejos. 🧭 Menús desplegables. ❤️ Botones de favoritos. 🛒 Carritos de compra. 🔍 Buscadores con autocompletado. 🎛️ Filtros dinámicos. El navegador puede mostrar la estructura, pero la aplicación todavía puede necesitar ejecutar JavaScript para activar el comportamiento esperado. ⚛️ ¿Cómo encaja React en todo esto? En una aplicación React que utiliza renderizado desde el servidor, el servidor puede generar HTML a partir de los componentes. Simplificando mucho, podríamos tener un componente como: function Comprar() { return ( <button onClick={() => console.log("Comprado")}> Comprar </button> ); } El servidor puede producir inicialmente algo parecido a: <button>Comprar</button> Ese HTML llega al navegador. Pero el navegador no recibe el onClick directamente como una función ejecutable dentro de ese HTML. React necesita ejecutarse en el cliente y relacionar ese nodo existente con el componente correspondiente. Después de la hidratación, React ya puede manejar la interacción. La idea importante es esta: 👉 React intenta reutilizar el HTML que ya fue renderizado en lugar de volver a construir toda la interfaz desde cero. 🔄 Hydration no es lo mismo que renderizar nuevamente toda la página Si el servidor ya produjo: <h1>Mi producto</h1> <button>Comprar</button> sería desperdiciar trabajo eliminar todo eso y volver a crearlo completamente desde JavaScript. La hidratación intenta aprovechar la estructura existente. De forma conceptual: HTML del servidor ↓ React encuentra estructura equivalente ↓ Conecta componentes y lógica ↓ Continúa trabajando desde ahí Por eso servidor y cliente necesitan estar suficientemente sincronizados respecto a lo que esperan renderizar. ⚠️ ¿Qué es un Hydration Mismatch? Aquí aparece un problema bastante conocido. Supongamos que el servidor genera: <p>Buenos días</p> Pero cuando React comienza a ejecutarse en el navegador, espera: <p>Buenas noches</p> Ahora existe una diferencia entre lo que produjo el servidor y lo que espera el cliente. Eso puede provocar un hydration mismatch . Otro ejemplo común sería generar contenido utilizando directamente valores que pueden cambiar entre servidor y navegador: const ahora = new Date(); El servidor podría renderizar: 10:59:59 y unos milisegundos después el navegador producir: 11:00:00 Dependiendo del framework y la situación, estas diferencias pueden producir advertencias, correcciones adicionales o incluso obligar a reconstruir partes de la interfaz. Por eso la hidratación también exige cierta consistencia. 🧠 El estado también debe tener sentido en ambos lados Imagina una aplicación que renderiza desde el servidor: Carrito: 3 productos Cuando JavaScript comienza a ejecutarse, el cliente necesita arrancar desde un estado compatible con lo que el usuario ya está viendo. Si el cliente cree inicialmente: Carrito: 0 productos pueden aparecer cambios visuales inesperados. Por eso algunas arquitecturas transmiten al navegador información necesaria para reconstruir el estado inicial. Conceptualmente: Servidor ↓ HTML + Datos iniciales ↓ Navegador ↓ Framework ↓ Hydration No se trata solamente de conectar botones. También puede involucrar reconstruir correctamente la aplicación alrededor del contenido ya renderizado. ⏱️ Ver rápido y responder rápido son métricas diferentes Tradicionalmente era común pensar: “La página cargó cuando apareció el contenido.” Pero la experiencia real es más compleja. Puedes tener una página que muestre algo muy rápido: 0.8 segundos → contenido visible pero que no responda correctamente hasta: 2.5 segundos → interacción disponible Desde una perspectiva técnica, la página puede parecer rápida. Desde la perspectiva del usuario: “Presioné el botón y no funcionó.” Y esa percepción importa muchísimo. Por eso el rendimiento frontend no solamente depende de cuánto tarda en aparecer contenido. También importa cuánto tarda la página en estar preparada para responder. 📦 El problema de enviar demasiado JavaScript Uno de los principales factores que puede retrasar la interactividad es la cantidad de JavaScript enviada al navegador. Imagina una página que descarga: 50 KB de JavaScript y otra que descarga: 2 MB de JavaScript No solamente cambia el tiempo de descarga. Ese código también necesita ser: Descargado ↓ Descomprimido ↓ Analizado ↓ Compilado ↓ Ejecutado ↓ Utilizado para hidratar componentes Por eso reducir JavaScript puede tener beneficios incluso en conexiones rápidas. La red es solamente una parte del costo. 📱 En dispositivos lentos el problema se vuelve más evidente Supongamos que pruebas una aplicación en una laptop moderna. Procesador rápido. Buena conexión. Memoria suficiente. La hidratación tarda tan poco que apenas la notas. Pero después alguien abre la misma página desde: 📱 Un teléfono económico. 📶 Una conexión móvil inestable. 🔋 Un dispositivo trabajando en modo de ahorro de energía. Ahora interpretar y ejecutar JavaScript puede tardar significativamente más. Esto explica por qué evaluar únicamente desde nuestra computadora de desarrollo puede producir una impresión engañosa. Una aplicación debería medirse también bajo condiciones más cercanas a las de usuarios reales. 🚧 El hilo principal del navegador también tiene trabajo JavaScript normalmente compite por recursos dentro del navegador con otras tareas. El navegador necesita: Interpretar HTML. Procesar CSS. Calcular estilos. Dibujar la interfaz. Ejecutar JavaScript. Responder a eventos. Actualizar elementos visuales. Si ejecutamos demasiado JavaScript durante el arranque, podemos bloquear temporalmente el hilo principal. Mientras está ocupado, el navegador puede tardar más en responder a interacciones. Conceptualmente: Usuario hace clic ↓ Evento esperando ↓ JavaScript pesado sigue ejecutándose ↓ Termina el trabajo ↓ Evento puede procesarse Entonces incluso una interfaz que técnicamente ya tiene lógica conectada puede sentirse lenta si el navegador continúa demasiado ocupado. 🧩 No toda la página necesita necesariamente Hydration Esta es una idea importante en arquitecturas modernas. Imagina un artículo como este. La mayoría de su contenido es: 📝 Texto. 🖼️ Imágenes. 📚 Encabezados. Eso no necesita necesariamente JavaScript para funcionar. Quizá solamente tengas: ❤️ Botón de favorito 💬 Comentarios 🔍 Buscador 🌙 Selector de tema Esos pequeños elementos sí necesitan interactividad. Entonces aparece una pregunta arquitectónica interesante: 👉 ¿Por qué enviar JavaScript para toda la página si solamente una pequeña parte realmente lo necesita? Algunos frameworks modernos permiten reducir la cantidad de componentes que necesitan ejecutarse en el cliente. 🏝️ El concepto de “islas de interactividad” Una estrategia relacionada consiste en mantener la mayor parte de una página como contenido estático y solamente activar pequeñas áreas interactivas. Conceptualmente: ┌──────────────────────────────┐ │ Contenido estático │ │ │ │ Texto │ │ Imagen │ │ Descripción │ │ │ │ ┌────────────────┐ │ │ │ 🛒 Carrito │ │ │ │ Interactivo │ │ │ └────────────────┘ │ │ │ └──────────────────────────────┘ En lugar de hidratar absolutamente todo, solamente determinadas “islas” necesitan JavaScript. No todos los frameworks implementan exactamente el mismo modelo, pero la idea general es útil: 👉 Enviar interactividad solamente donde realmente aporta valor. ⚙️ Server Components también cambian cuánto JavaScript necesita el cliente En ecosistemas modernos como React y Next.js han aparecido enfoques donde determinados componentes pueden ejecutarse únicamente en el servidor. Eso permite que partes de la interfaz no necesiten convertirse automáticamente en JavaScript ejecutándose en el navegador. Conceptualmente: Server Component ↓ Renderiza contenido ↓ No necesita interactividad cliente Client Component ↓ Necesita estado / eventos / APIs del navegador ↓ JavaScript en el cliente Por ejemplo, un componente que solamente muestra: function Titulo({ producto }) { return <h1>{producto.nombre}</h1>; } puede no necesitar lógica interactiva en el cliente. Mientras que un componente como: function Contador() { const [cantidad, setCantidad] = useState(0); return ( <button onClick={() => setCantidad(cantidad + 1)}> {cantidad} </button> ); } sí necesita ejecutarse allí. La idea es reducir el área interactiva cuando sea posible. 🔄 Existen diferentes estrategias de Hydration No todas las aplicaciones tienen que hidratar toda la interfaz inmediatamente. Dependiendo del framework y arquitectura pueden existir estrategias como: Hydration completa Gran parte de la aplicación se hidrata al cargar. Hydration selectiva Algunas partes pueden priorizarse antes que otras. Hydration progresiva La interactividad puede ir activándose gradualmente. Hydration bajo demanda Ciertos componentes pueden esperar hasta que realmente sean necesarios. Las implementaciones concretas varían bastante entre herramientas y versiones. Pero todas intentan resolver una idea parecida: 👉 No hacer todo el trabajo de JavaScript de golpe si no es necesario. 👀 ¿Qué debería priorizarse primero? Imagina una página larga. En la parte superior tienes: 🔍 Buscador 🛒 Carrito 🔐 Iniciar sesión Y mucho más abajo: ⭐ Reseñas 📊 Gráficas 💬 Comentarios No necesariamente todas esas funciones tienen la misma prioridad. Tiene sentido que el navegador dedique recursos primero a aquello con lo que probablemente vas a interactuar inmediatamente. El resto puede esperar dependiendo de la arquitectura. Esta forma de pensar ayuda a diseñar aplicaciones que no solamente cargan rápido, sino que utilizan mejor el trabajo disponible en el navegador. ⚠️ Error común: pensar que SSR resuelve automáticamente el rendimiento Puedes pensar: “Estoy renderizando desde el servidor, entonces mi aplicación ya es rápida.” No necesariamente. El servidor puede mejorar el tiempo en que aparece contenido, pero si después envías una enorme cantidad de JavaScript, todavía puedes tener problemas de interactividad. Podrías tener: HTML aparece rápido ✅ JavaScript enorme ❌ Hydration lenta ❌ Interacción tardía ❌ SSR y Hydration forman parte del panorama, pero no garantizan por sí solos una buena experiencia. 📊 ¿Cómo medir si realmente existe un problema? No conviene optimizar solamente por intuición. Puedes utilizar herramientas de rendimiento del navegador y métricas web para entender qué ocurre realmente. Una métrica especialmente relacionada con la capacidad de respuesta es Interaction to Next Paint (INP) . INP intenta reflejar qué tan rápido responde una página a las interacciones del usuario durante su visita. No mide exclusivamente Hydration, pero una cantidad excesiva de JavaScript y trabajo en el hilo principal puede afectar la capacidad de responder. También resulta útil analizar: Tamaño de bundles JavaScript. Long Tasks. Uso del hilo principal. Tiempo de ejecución de scripts. Rendimiento bajo CPU limitada. Rendimiento en redes más lentas. El objetivo no es simplemente conseguir una puntuación. Es identificar qué está retrasando la experiencia. 🛠️ ¿Cómo podemos reducir el costo de Hydration? No existe una solución única, pero hay varias prácticas que pueden ayudar dependiendo de la aplicación. 1. Enviar menos JavaScript Si una funcionalidad no necesita ejecutarse en el navegador, evita convertirla innecesariamente en código cliente. 2. Mantener pequeños los componentes interactivos En lugar de volver interactiva toda una sección enorme por un solo botón, puede ser conveniente aislar la lógica. Por ejemplo: Página completa ├── Texto estático ├── Imagen estática ├── Descripción estática └── Botón interactivo 3. Dividir código No toda funcionalidad necesita descargarse al inicio. Herramientas de code splitting permiten cargar determinadas partes solamente cuando son necesarias. 4. Evitar trabajo pesado al arrancar Si ejecutas cálculos costosos inmediatamente después de cargar la página, puedes competir directamente con el trabajo necesario para volverla interactiva. 5. Mantener consistencia entre servidor y cliente Evita producir HTML diferente en ambos lados cuando no sea necesario. Esto reduce problemas de Hydration Mismatch. 6. Probar en hardware menos potente Una página que funciona perfectamente en tu computadora puede comportarse de manera completamente diferente en dispositivos modestos. 🚀 Ejemplo de una arquitectura más eficiente Imagina una tienda online. Podrías dividir conceptualmente la página así: Página de producto │ ├── Server │ ├── Nombre │ ├── Precio │ ├── Descripción │ ├── Características │ └── Información SEO │ └── Cliente ├── Selector de cantidad ├── Botón agregar al carrito ├── Favoritos └── Galería interactiva En lugar de enviar toda la página como una gran aplicación JavaScript, puedes reservar el trabajo cliente para las partes que realmente necesitan interacción. Esto puede reducir: 📦 JavaScript enviado. ⚙️ Trabajo de ejecución. 💧 Trabajo de hidratación. ⏱️ Tiempo hasta responder. No siempre será la arquitectura correcta para todos los proyectos, pero muestra cómo pensar en el problema. 🧩 La realidad Cuando una página aparece en tu pantalla, eso no significa necesariamente que el navegador haya terminado todo el trabajo. Puede haber ocurrido esto: HTML llegó ↓ Contenido apareció ↓ 👀 "Parece cargada" ↓ JavaScript sigue trabajando ↓ Hydration ↓ Eventos conectados ↓ 🖱️ "Ahora sí puedo usarla" La diferencia puede durar unos pocos milisegundos y pasar completamente desapercibida. O puede durar suficiente tiempo como para que el usuario presione un botón y piense que la aplicación está rota. Por eso una interfaz rápida no debería medirse únicamente preguntando: “¿Cuándo apareció?” También deberíamos preguntar: 👉 ¿Cuándo pudo el usuario interactuar con ella sin fricción? Hydration es una de esas piezas de frontend que normalmente permanece invisible cuando todo funciona bien. Pero cuando hacemos demasiado trabajo en el navegador, esa pequeña diferencia entre ver y usar puede convertirse en uno de los problemas más perceptibles de toda la experiencia. 💬 Una buena experiencia web no solamente muestra contenido rápido. También responde rápido cuando el usuario intenta utilizarlo. 👉 ¿Alguna vez intentaste presionar un botón apenas apareció una página y tuviste que hacerlo nuevamente porque todavía no estaba lista? 🔥 El backend no se ve, pero sin él, nada funciona.

10 sep 2026
Leer publicación
¿Cómo puede una aplicación guardar millones de imágenes y archivos sin llenar sus servidores?
Arquitectura de Software

¿Cómo puede una aplicación guardar millones de imágenes y archivos sin llenar sus servidores?

📸 Subes una foto a una aplicación… pero ¿dónde termina realmente ese archivo? Cambias tu foto de perfil. Seleccionas una imagen desde tu teléfono. Presionas “Guardar”. Unos segundos después, la fotografía aparece en tu cuenta y puedes verla desde cualquier dispositivo. Parece algo sencillo. Pero ahora imagina una plataforma con millones de usuarios haciendo exactamente lo mismo. Todos los días pueden subir: 🖼️ Fotografías. 🎥 Videos. 📄 Documentos. 🎧 Audios. 📦 Copias de seguridad. 🗂️ Archivos generados por la propia aplicación. Cuando estás construyendo un proyecto pequeño, guardar esos archivos directamente en el disco del servidor puede parecer perfectamente razonable. Por ejemplo: /app/uploads/perfiles/foto.webp El problema aparece cuando la aplicación crece. Miles de archivos se convierten en millones. Un servidor se convierte en varios. Y ese sencillo directorio /uploads empieza a convertirse en una dependencia importante de toda la arquitectura. 👉 ¿Cómo almacenan entonces las aplicaciones grandes cantidades de archivos sin convertir sus servidores de backend en enormes discos duros? Una de las respuestas más comunes es el almacenamiento de objetos u Object Storage . 🧠 ¿Qué es el almacenamiento de objetos? Object Storage es un modelo diseñado para almacenar grandes cantidades de datos no estructurados como imágenes, videos, documentos, audios o respaldos. En lugar de depender del disco donde está ejecutándose tu aplicación, utilizas un sistema especializado para almacenar esos archivos. Servicios como Amazon S3, Google Cloud Storage, Azure Blob Storage o soluciones compatibles como MinIO utilizan este tipo de enfoque. La idea general es separar dos responsabilidades: Backend ↓ Lógica de la aplicación Object Storage ↓ Archivos Tu backend continúa decidiendo quién puede subir una imagen, qué formatos acepta, qué usuario es propietario del archivo o qué permisos existen. Pero ya no necesita almacenar físicamente todos esos datos en su propio disco. 📦 ¿Qué significa que un archivo sea un “objeto”? En un almacenamiento tradicional solemos pensar en: Carpeta └── Subcarpeta └── archivo.jpg En Object Storage, el modelo conceptual es diferente. Un objeto normalmente contiene o está asociado con elementos como: 📦 Datos El contenido del archivo 🏷️ Metadatos Información relacionada con el objeto 🔑 Clave Un identificador utilizado para localizarlo Por ejemplo, una imagen podría tener una clave como: usuarios/847/perfil.webp También puede existir información asociada como su tipo de contenido: Content-Type: image/webp Además, los objetos suelen almacenarse dentro de un contenedor lógico que, dependiendo del proveedor, puede recibir nombres como bucket o container . ⚙️ ¿Qué ocurre cuando subes una foto? Imagina una aplicación donde el usuario cambia su fotografía de perfil. Un flujo sencillo podría ser: 👤 Usuario ↓ 🌐 Frontend ↓ ⚙️ Backend ↓ ☁️ Object Storage ↓ 🔑 Clave del objeto ↓ 🗄️ Base de datos El usuario selecciona la imagen. El backend recibe la solicitud y puede validar aspectos como: Si el usuario está autenticado. Si tiene permiso para realizar la operación. Si el archivo supera el tamaño permitido. Si el formato está permitido. Qué nombre o clave utilizará. Después, el archivo puede almacenarse en Object Storage. El almacenamiento podría devolver o permitir construir una referencia como: usuarios/847/perfil.webp Y esa referencia puede relacionarse con el usuario dentro de la base de datos. 🗄️ La base de datos no necesita contener la imagen completa Este punto es importante. Supongamos que tenemos una tabla de usuarios. Podríamos guardar algo conceptualmente parecido a: id: 847 nombre: "Carlos" foto_perfil: "usuarios/847/perfil.webp" La base de datos almacena información estructurada sobre el usuario y una referencia al archivo. Mientras tanto: Base de datos ↓ "usuarios/847/perfil.webp" Object Storage ↓ 📸 Datos reales de la fotografía Eso no significa que sea técnicamente imposible guardar archivos binarios dentro de una base de datos relacional. Existen tipos de datos que lo permiten y escenarios donde puede tener sentido. Pero para grandes cantidades de imágenes, videos y archivos similares, separar los archivos hacia almacenamiento especializado suele simplificar la escalabilidad y operación del sistema. 💾 ¿Por qué no guardar simplemente todo en el servidor? Al principio puede funcionar perfectamente. Tienes una aplicación pequeña: Servidor ├── Backend ├── Base de datos └── uploads/ ├── foto1.jpg ├── foto2.jpg └── foto3.jpg Quizá solamente existen unos cientos de archivos. No parece existir ningún problema. Ahora supongamos que la plataforma alcanza: 1,000,000 imágenes y cada imagen ocupa aproximadamente: 5 MB Estamos hablando de alrededor de: 5,000,000 MB ≈ 5 TB Y eso solamente considerando las imágenes. Agrega videos de cientos de megabytes, documentos, audios, archivos generados automáticamente y nuevos usuarios todos los días. El almacenamiento deja rápidamente de ser un detalle menor. 🚀 El verdadero problema aparece cuando agregas más servidores La capacidad del disco no es el único problema. Imagina que inicialmente tienes: Usuario ↓ Servidor A ↓ /uploads Todo funciona. Pero la aplicación crece y necesitas ejecutar tres instancias del backend: ┌→ Backend A Usuario → Balanceador → Backend B └→ Backend C Un usuario sube una imagen y su petición termina en Backend A . Si guardas el archivo únicamente en su disco: Backend A /uploads/foto.webp ✅ Backend B /uploads/foto.webp ❌ Backend C /uploads/foto.webp ❌ Después otra petición podría llegar al Backend C. Ahora aparece una pregunta incómoda: 👉 ¿Dónde está realmente la fotografía? Puedes diseñar almacenamiento compartido de otras maneras, por supuesto. Object Storage no es la única solución posible. Pero separar el almacenamiento persistente de las instancias de aplicación elimina esa dependencia del disco local. ☁️ Todos los backends pueden utilizar el mismo almacenamiento Con almacenamiento externo, el diseño cambia: ┌→ Backend A ─┐ Usuario ─────┼→ Backend B ─┼→ ☁️ Object Storage └→ Backend C ─┘ Ahora ninguna instancia necesita ser “la dueña” permanente del archivo. Los servidores de backend pueden incluso crearse, reiniciarse o eliminarse sin que eso implique perder las fotografías de los usuarios. Esta separación es especialmente importante en infraestructuras donde las instancias de aplicación pueden ser reemplazables. El backend procesa solicitudes. El almacenamiento mantiene los archivos persistentes. 🔑 ¿Cómo identificamos millones de objetos? Guardar millones de archivos también obliga a pensar correctamente en sus claves. Una opción podría parecer: foto.jpg Pero rápidamente aparecen conflictos. ¿Qué ocurre cuando 50,000 usuarios suben un archivo llamado exactamente foto.jpg ? Por eso las aplicaciones suelen generar claves que eviten colisiones. Por ejemplo: usuarios/847/550e8400-e29b-41d4-a716-446655440000.webp o una estructura similar a: perfiles/847/avatar-20260909.webp La estrategia exacta depende de la aplicación. Lo importante es no depender ciegamente del nombre original proporcionado por el usuario. 📂 Pero esas “carpetas” no funcionan exactamente como las de tu computadora En una interfaz de almacenamiento podrías ver: usuarios/ └── 847/ └── fotos/ └── verano.jpg Visualmente parece un sistema tradicional de carpetas. Pero muchos sistemas de Object Storage trabajan fundamentalmente con claves dentro de un espacio de nombres plano. Por ejemplo: usuarios/847/fotos/verano.jpg puede ser simplemente la clave completa del objeto. Los / permiten utilizar prefijos y presentar los objetos de una forma parecida a directorios. Conceptualmente podríamos tener: usuarios/847/fotos/verano.jpg usuarios/847/fotos/invierno.jpg usuarios/847/documentos/cv.pdf y buscar objetos que comiencen con: usuarios/847/fotos/ La interfaz puede mostrarlos como una carpeta, aunque internamente el modelo no tenga por qué ser idéntico a un sistema de archivos tradicional. 🔗 El archivo ni siquiera tiene que pasar por tu backend Ahora imagina que los usuarios pueden subir videos de: 500 MB El flujo más sencillo podría ser: Usuario ↓ Frontend ↓ Backend ↓ Object Storage Eso significa que los 500 MB atraviesan tu servidor de aplicación antes de llegar al almacenamiento. Si muchos usuarios hacen lo mismo simultáneamente, el backend termina utilizando ancho de banda y recursos simplemente para transportar archivos. Existe otra estrategia muy utilizada: las URLs firmadas o presigned URLs . 🔐 ¿Cómo funciona una URL firmada? El backend sigue participando en la autorización, pero no necesariamente transporta todo el archivo. Un flujo conceptual podría ser: 👤 Usuario ↓ ⚙️ Solicita permiso al backend ↓ 🔐 Backend genera autorización temporal ↓ 🌐 Frontend recibe URL firmada ↓ ☁️ Sube directamente al Object Storage Por ejemplo, el backend puede decidir: Este usuario tiene permiso para subir este objeto durante los próximos minutos. La aplicación recibe una URL temporal y utiliza algo parecido a: PUT https://storage.example/... Content-Type: image/webp para enviar directamente el archivo al servicio de almacenamiento. La URL puede expirar después de cierto tiempo. Así no necesitas hacer público todo el almacenamiento simplemente para permitir una carga. ⚠️ Una URL firmada no significa abandonar las validaciones Permitir cargas directas no significa: “Aquí tienes acceso al almacenamiento, sube cualquier cosa.” El backend todavía debería controlar aspectos importantes. Por ejemplo: ✔️ Qué usuario solicita la operación. ✔️ Qué tipo de archivo puede subir. ✔️ Qué tamaño máximo permites. ✔️ Qué objeto puede modificar. ✔️ Durante cuánto tiempo será válida la autorización. ✔️ Qué permisos tendrá posteriormente el archivo. Además, según el nivel de riesgo de la aplicación, los archivos pueden necesitar procesamiento adicional, análisis de seguridad o validaciones posteriores a la carga. La arquitectura cambia quién transporta los bytes. No elimina las reglas de seguridad. 🖼️ ¿Y qué ocurre después de subir una imagen? En aplicaciones reales, almacenar el archivo original puede ser solamente el comienzo. Imagina que alguien sube una fotografía de 8 MB y resolución enorme. Probablemente no quieras enviar esa imagen completa cada vez que necesites mostrar un avatar de 80×80 píxeles. Puedes tener un proceso como: 📸 Imagen original ↓ ☁️ Object Storage ↓ ⚙️ Procesamiento ↓ ├── perfil-80.webp ├── perfil-300.webp └── perfil-1200.webp El procesamiento puede ocurrir de forma independiente al servidor que atiende la petición original. Esto permite crear miniaturas, convertir formatos, comprimir archivos o generar diferentes versiones según las necesidades del producto. 🌍 Guardar el archivo y entregarlo son problemas diferentes Object Storage resuelve principalmente el almacenamiento. Pero supongamos que tienes usuarios en diferentes regiones solicitando millones de imágenes. Si todas las descargas tienen que llegar siempre directamente al mismo origen, puedes tener problemas de latencia y tráfico. Aquí puede entrar una CDN . El flujo conceptual cambia a: ☁️ Object Storage ↓ 🌍 CDN ↓ 👤 Usuarios La CDN puede mantener copias temporales del contenido en ubicaciones más cercanas a los usuarios. Así, un archivo solicitado frecuentemente no necesariamente necesita viajar desde el almacenamiento de origen en cada petición. Object Storage y CDN resuelven problemas relacionados, pero diferentes: Object Storage → ¿Dónde guardo el archivo? CDN → ¿Cómo lo distribuyo eficientemente? 🔒 No todos los archivos deberían ser públicos Una fotografía pública de un producto y un documento privado de un usuario tienen requisitos completamente diferentes. Por eso es importante definir correctamente los permisos. Puedes tener objetos públicos cuando realmente deban ser accesibles por cualquiera. Pero para contenido privado puede ser preferible mantener el almacenamiento cerrado y proporcionar acceso controlado. Por ejemplo: Usuario autenticado ↓ Backend verifica permisos ↓ ¿Puede descargar el documento? ↓ Sí ↓ URL temporal ↓ Descarga Así, conocer el nombre del archivo no necesariamente debería ser suficiente para acceder a él. 🗑️ También necesitas pensar en el ciclo de vida de los archivos Guardar objetos parece barato cuando solamente tienes unos cuantos gigabytes. El problema aparece cuando acumulas terabytes de archivos que nadie utiliza. Por ejemplo: Un usuario cambia su avatar diez veces. ¿Necesitas conservar para siempre las diez versiones anteriores? Una aplicación genera archivos temporales. ¿Quién los elimina? Creas copias de seguridad. ¿Cuánto tiempo deben conservarse? Un buen sistema necesita políticas de ciclo de vida. Conceptualmente: Archivo temporal ↓ 30 días sin utilizarse ↓ Eliminar automáticamente O: Backup reciente ↓ Después de cierto tiempo ↓ Mover a almacenamiento más económico Las posibilidades concretas dependen del proveedor y de las necesidades del proyecto. 💰 Object Storage también tiene costos que debes entender Mover los archivos fuera del servidor no significa que el almacenamiento sea gratuito o ilimitado. Dependiendo del proveedor puedes pagar por diferentes conceptos: 💾 Cantidad de información almacenada. 📥 Operaciones realizadas. 🌐 Transferencia de datos. 🔄 Recuperación de determinados tipos de almacenamiento. 📦 Características adicionales. Una arquitectura puede funcionar técnicamente muy bien y aun así resultar costosa si mueve enormes cantidades de información innecesariamente. Por eso también conviene pensar en compresión, formatos adecuados, caché, políticas de retención y patrones de acceso. ⚠️ Errores comunes al manejar archivos Uno de los más evidentes es depender permanentemente del disco local del backend en una arquitectura donde las instancias deberían ser reemplazables. Pero no es el único. También aparecen errores como: Guardar el nombre proporcionado por el usuario sin una estrategia segura para identificar objetos. Dejar todos los archivos públicos por comodidad. No limitar tamaños de carga. Confiar únicamente en la extensión: foto.jpg sin realizar las validaciones necesarias. Olvidar eliminar archivos que ya no tienen referencias. Guardar una nueva imagen en Object Storage, pero fallar antes de registrar correctamente su relación en la base de datos. O hacer lo contrario: Base de datos: foto = "usuarios/847/perfil.webp" Object Storage: ❌ El objeto ya no existe Cuando dos sistemas almacenan partes relacionadas de la información, también necesitas pensar en cómo mantenerlas consistentes. 🛠️ Buenas prácticas Cuando una aplicación comienza a manejar una cantidad considerable de archivos, algunas prácticas útiles son: ✔️ Separar el almacenamiento persistente del disco local de las instancias del backend cuando la arquitectura lo requiera. ✔️ Utilizar claves controladas por la aplicación y evitar depender únicamente del nombre original. ✔️ Guardar en la base de datos las referencias y metadatos que realmente necesites. ✔️ Definir límites de tamaño y formatos aceptados. ✔️ Mantener privados los objetos que no necesitan acceso público. ✔️ Utilizar URLs firmadas para cargas o descargas temporales cuando tenga sentido. ✔️ Procesar imágenes y videos en tamaños adecuados para su uso. ✔️ Establecer políticas para archivos temporales, antiguos o abandonados. ✔️ Utilizar una CDN cuando exista contenido público con mucho tráfico. ✔️ Monitorear no solamente cuánto almacenas, sino también cuánto transfieres. 🧩 Una arquitectura más realista Juntando varias de estas ideas, una aplicación podría terminar utilizando un flujo parecido a este: ┌─────────────────┐ │ Usuario │ └────────┬────────┘ │ ↓ ┌─────────────────┐ │ Frontend │ └────────┬────────┘ │ solicita permiso ↓ ┌─────────────────┐ │ Backend │ └──────┬─────┬────┘ │ │ metadatos │ │ autorización ↓ ↓ ┌─────────┐ 🔐 │ Base de │ │ datos │ └─────────┘ Usuario ───── carga directa ───→ ☁️ Object Storage │ ↓ ⚙️ Procesamiento │ ↓ 🌍 CDN │ ↓ Usuarios No todas las aplicaciones necesitan esta arquitectura completa. Un proyecto pequeño puede funcionar perfectamente con algo mucho más sencillo. El objetivo no es utilizar servicios adicionales porque “así lo hacen las empresas grandes”. El objetivo es entender qué problema resuelve cada componente y agregarlo cuando realmente sea necesario. 🧩 La realidad Una aplicación puede manejar millones de imágenes sin que sus servidores de backend necesiten almacenar físicamente cada archivo en sus propios discos. La clave está en separar responsabilidades. ⚙️ Backend Lógica, permisos y reglas de negocio 🗄️ Base de datos Información estructurada y referencias ☁️ Object Storage Imágenes, videos, documentos y otros objetos ⚙️ Procesamiento Transformaciones cuando sean necesarias 🌍 CDN Distribución eficiente del contenido Object Storage no significa simplemente “tener un disco duro más grande en la nube”. Representa una forma diferente de diseñar el almacenamiento para que los archivos no estén atados al ciclo de vida de un servidor específico. Y cuando una aplicación empieza a crecer, esa diferencia se vuelve cada vez más importante. 💬 Escalar archivos también significa dejar de tratarlos como si toda tu aplicación siguiera viviendo dentro de una sola computadora. 👉 En tu último proyecto, ¿los archivos terminaron dentro de /uploads en el servidor o ya separaste el almacenamiento de tu backend? 🔥 El backend no se ve, pero sin él, nada funciona.

09 sep 2026
Leer publicación

Contacto

¿Tienes una idea, proyecto, propuesta laboral o colaboración?

Escríbeme un mensaje por WhatsApp.

Información de contacto

Redes

Mensaje rápido

Generar mensaje para WhatsApp

Llena los campos y se abrirá WhatsApp con un mensaje listo para enviar.