- La cultura de desarrollo donde instalar paquetes se volvió la opción por defecto alimenta el dependency churn, que deriva en actualizaciones, parches, auditorías y gestión de dependencias transitivas, creando un costo oculto para la productividad
- Mientras más maduro es el ecosistema de empaquetado, como en JavaScript y Rust, mayor es el impacto; hay casos como un proyecto nuevo de Tokio con 28 crates, Rocket con 172 crates y la CLI de MiniJinja con 142 dependencias
- Incluso una función estable desde hace mucho, como
terminal_size, cae en la paradoja de arrastrar crates adicionales y lanzamientos repetidos por cambios en las bibliotecas de abstracción de plataforma
- Para funciones pequeñas, crear una implementación sin dependencias con ChatGPT o Cursor puede ser más rápido que buscar dependencias y mantener actualizaciones continuas, además de reducir la carga de compilar código externo
- Bibliotecas para áreas difíciles como HTTP, QUIC, gráficos o tokio sí son necesarias, pero conviene cuestionar más la decisión de aceptar un gran grafo de dependencias por una sola función
El treadmill de mantenimiento que provoca el aumento de dependencias
- Los desarrolladores instalan paquetes fácilmente por productividad, pero como resultado terminan atrapados en un dependency churn interminable de actualizaciones, parches, auditorías y gestión de dependencias transitivas
- Este problema destaca más en ecosistemas con buenas soluciones de empaquetado, y JavaScript y Rust se ven especialmente afectados
- Casos en Rust:
- Un proyecto nuevo con Tokio trae 28 crates
- Un proyecto nuevo con Rocket sube hasta 172 crates
- MiniJinja puede existir con una sola dependencia, pero su variante CLI trae 142 dependencias
- El problema central es que, por una función pequeña, se termina compilando y administrando mucho más código externo del realmente necesario
La paradoja del código estable que muestra terminal_size
terminal_size es, tal como indica su nombre, un crate para detectar el tamaño de la terminal
- La API base que usa esta función ha sido en la práctica estable desde los inicios de las terminales informáticas, pero según el sistema operativo arrastra 3 o 4 crates adicionales
- Se llega a la situación de compilar miles de otras funciones solo para verificar si la terminal es 80x25 o 120x40
- Ese crate ha tenido 26 lanzamientos, pero una implementación propia puesta en un proyecto hace 10 años para la misma función sigue funcionando sin actualizaciones
- La razón de tantos lanzamientos no es que la función haya cambiado, sino que las bibliotecas de abstracción de plataforma subyacentes siguen cambiando
- En UNIX existe la excepción de necesitar una dependencia de
libc
- Rust no expone las constantes de libc de la plataforma, y esas constantes además no están estandarizadas
- Aun así,
libc es una dependencia común y ligera, por lo que es difícil evitarla
La seguridad y la cultura de reutilización de código refuerzan las dependencias
- El enfoque tipo “big supply chain” desincentiva copiar y pegar funciones o usar
unsafe directamente, y presiona para delegar en capas de abstracción de plataforma
- También existen empresas que ofrecen herramientas para tratar problemas de dependencias y, en nombre de la seguridad, empujan a mantenerlas y actualizarlas constantemente
- Pero muchas dependencias en sí mismas pueden ser una fuente principal de problemas de seguridad
- El objetivo del código debería ser escribirse de forma que en algún momento alcance estabilidad y ya no necesite actualizaciones
- En el ecosistema Rust, incluso una dependencia que funciona de forma estable puede recibir una mala evaluación en RUSTSEC si su bug tracker luce algo inactivo
- La cultura corporativa de code review también influye en el open source
- Es más probable que se recompense que se cuestione a un ingeniero que trae una biblioteca nueva y reluciente
- Como resultado surgieron herramientas como Dependabot, y los proyectos reciben PR continuos de actualización de dependencias
- Dentro de empresas, solo con vendoring, auditorías internas y upgrades a nivel organización ya se puede mantener ocupados de forma permanente a los equipos de ingeniería
Las funciones pequeñas se pueden hacer directamente
- Un camino más simple es escribir directamente el código que se necesita
- Puede requerir más trabajo inicial, pero una vez escrito, ese código no necesita crates nuevos ni obliga a esperar a que un autor upstream corrija edge cases
- Si el código se rompe en su propio uso, se corrige directamente, y el código que funciona no tiene por qué subirse obligatoriamente al treadmill de mantenimiento
- En 2025, herramientas como ChatGPT o Cursor pueden generar rápido una implementación sin dependencias para funciones pequeñas y comunes
- Muchas funciones pequeñas tienen un overhead de mantenimiento bajo, y pueden resultar menos costosas que las actualizaciones continuas de dependencias
- Si se trata de unas cuantas líneas de código, no hace falta compilar miles de líneas ajenas por una sola función
Valorar más las bajas dependencias
- No todas las dependencias son malas
- Bibliotecas gráficas que abstraen drivers complejos
- Implementaciones de protocolos como HTTP y QUIC
- Bibliotecas importantes como tokio, que no se pueden quitar ni hay intención de quitarlas
- Aun así, si por usar una sola función terminas compilando cientos de funciones, eso debería verse como una señal de alerta
- Deberíamos reconocer más la decisión de escribir funciones pequeñas directamente para evitar el grafo de dependencias transitivas
- También deberíamos mirar con más sospecha los grandes grafos de crates y valorar positivamente el código simple y estable que puede quedarse años sin tocarse
sha1-smol se volvió originalmente, bajo el nombre sha1, el crate estándar para calcular hashes SHA1, pero después cedió ese nombre a rust-crypto y recibió presión para alinearse con un ecosistema crypto más grande
- Si se usa el nuevo crate
sha1, vienen 10 dependencias adicionales
- Fue difícil evitarlo porque el nombre del registry importaba y había exigencias de compatibilidad con traits
- MiniJinja destaca en su README sus pocas dependencias
$ cargo tree
minimal v0.1.0 (examples/minimal)
└── minijinja v2.6.0 (minijinja)
└── serde v1.0.144
- MiniJinja tiene un PR para eliminar su última dependencia
- En las situaciones adecuadas, deberíamos celebrar el hazlo tú mismo y dar más reconocimiento a quienes crean bibliotecas open source con dependencias bajas o nulas
1 comentarios
Opiniones en Hacker News
Me gusta el lenguaje Rust en sí, pero no me gusta el ecosistema de dependencias de Rust. Mucha gente se queja de que en C++ es difícil agregar dependencias, pero más bien eso me parece una característica. Porque te obliga a pensar si realmente las necesitas
En C++ yo controlo las dependencias, pero en Rust enseguida pasan de 100 y termino rindiéndome. Desde el punto de vista de seguridad, honestamente no puedo saber qué estoy distribuyendo
Además, Rust no tiene compatibilidad ABI ni una cultura de bibliotecas compartidas. Por eso siento que rompe el modelo de distribución de paquetes del sistema operativo. Cuando eliges una distribución de Linux, estás confiando en las personas que construyen esa distribución; Rust se parece más a Python, donde cualquiera puede subir algo a PyPI
¿Y Docker/Python/Rust? No conozco en absoluto a quienes hicieron mi imagen de Docker, mi paquete de PyPI o mi crate de Rust
En la práctica, volvimos a la época de pasarnos EXE y DLL en archivos ZIP. Solo que ahora lo llamamos contenedor y lo ejecutamos orgullosamente con permisos de root
Como dice el autor, a veces lo mejor es simplemente incorporar el código fuente de las dependencias. Antes a esto se le llamaba vendoring y era bastante importante en Rails/Ruby. Tiene la gran ventaja de no verse afectado después por tomas maliciosas de paquetes y, si quieres, puedes fusionar tú mismo los parches de seguridad del upstream
No se me ocurre ningún proyecto en el que el síndrome NIH haya tenido un efecto neto positivo. Incluso las dependencias no esenciales ahorran muchísimo tiempo
Si en cada proyecto se rehace una parte de las dependencias opcionales, me pregunto de dónde sale el tiempo para corregir bugs adicionales, casos límite, múltiples versiones de dependencias transitivas y soporte para varias plataformas
Si a eso se le suma un repositorio donde cualquiera puede subir cosas y con poca curaduría, como PyPI, npm o Cargo, junto con una biblioteca estándar pequeña, hay que prepararse para sufrir
El desarrollador elige qué dependencias traer. Las dependencias que nadie usa desaparecerán, y la razón por la que la mayoría de las bibliotecas tienen muchas dependencias es que muchos desarrolladores prefieren construir cosas encima de dependencias
Si hablamos del sistema de build, Cargo no obliga a usar Crates.io. Solo lo facilita. También se pueden usar dependencias basadas en rutas, como con CMake/Vcpkg o Conan, o crear la biblioteca directamente
Incluso si usas Crates.io, si no te gustan los cambios, basta con fijar versiones. Solo facilita obtener la versión más reciente
En Rust es fácil crear software sobre software existente. Si no te gusta el software existente o su ritmo de cambio, no culpes a Cargo: hazlo de la forma que prefieras
Las distribuciones siguen compilando los paquetes de Rust desde el código fuente y vendorizan las dependencias de crates en el repositorio. Es más doloroso porque hay más dependencias y se actualizan con más frecuencia, pero eso es independiente de las bibliotecas compartidas
No es exacto decir que la API para averiguar el tamaño de la terminal ha sido estable durante 50 años. Hasta donde sé, TIOCGWINSZ ioctl nunca se estandarizó, y tiene varios nombres según Unix y BSD
La función
tcgetwinsize()recién entró en POSIX en 2024, y todo este tema tiene una historia bastante lamentable https://news.ycombinator.com/item?id=42039401. Es decir, esto ya pasa incluso antes de llegar al lado de WindowsA veces eso está bien, pero puede empeorar el software para personas con otros casos de uso o para quienes lo ejecutan en sistemas que el autor no entiende ni usa
El verdadero valor de una biblioteca está en encargarse de que incluso problemas que parecen simples en realidad tienen mucha complejidad. Tampoco creo que 3 o 4 dependencias en una biblioteca que corre tanto en Windows como en Linux sean demasiadas. En el mejor de los casos, quien usa esa biblioteca tiene más experiencia en esa área y puede encargarse de partes desconocidas para mí
Sin duda hay problemas con ioctl, pero los cambios que produjeron varias releases en
terminal-sizeo sus dependencias no tienen que ver con las constantesioctl/TIOCGWINSZni con la estructurawinsize. Ese código no cambióterminal-sizesimplemente llama atcgetwinsizede Rustix, y Rustix a su vez llama atcgetwinsizede libc. Así que, si haces lo mismo directamente, puedes reducir bastante las dependencias, y el costo sería básicamente el soporte para WindowsQue esta API haya sido estable durante 50 o 25 años es un detalle. Esa dependencia ni siquiera pretende manejar esa complejidad, y tampoco es probable que esa función cambie o desaparezca en el futuro cercano
La mayoría escribe directamente ahí, pero funciones como el tamaño de la terminal o el modo raw son un desastre y requieren cosas como
ioctl. Honestamente, no es muy buenoPero las bibliotecas son aún peores. Si no quieres enlazar con
ncurses, que Dios te ayudeHace poco reviví la primera webapp de mi startup, creada en 2006. Era un sitio de redes sociales centrado en compartir medios y, para los estándares de la época, era una pila LAMP bastante simple. Usaba PHP 5 y MySQL 3.2, pero tenía en general las funciones de redes sociales de ese momento.
Quería probar por mi cuenta nuevas tecnologías de CI/CD, así que estoy creando un proceso de despliegue usando esta app como proyecto de aprendizaje, con más ingeniería de la necesaria. Podría haber usado WordPress o una app Hello World, pero esto es mucho más divertido.
Casi todo el PHP lo escribí yo mismo. También hice mis propias bibliotecas para autenticación/autorización, plantillas, manejo de formularios, etc., y solo usé una biblioteca PEAR para el envío de correos. El frontend era HTML puro, casi sin JavaScript, y para reproducir medios usaba Flash. En 2006 la mayoría de las cosas se hacían así.
Volver a ejecutar una app de 19 años me tomó apenas alrededor de una hora. Cambié el viejo driver
mysqlde PHP pormysqliy ajusté el esquema y algunas consultas para MySQL 8. Principalmente fue encerrar con backticks palabras que ahora son reservadas y corregir valores predeterminados de columnas que se volvieron más estrictos. Lo único que no funcionó fue Flash.En cambio, en mi trabajo actual operamos decenas de apps Spring Boot escritas en Java 8, y tenemos páginas y páginas de vulnerabilidades derivadas de decenas de dependencias. Si actualizas una, también tienes que subir una cadena de otras bibliotecas, y por las dependencias transitivas se vuelve una pesadilla. Por eso solo atendemos lo mínimo de las vulnerabilidades más críticas, y no hay un plan realista para actualizar todo.
Lo gracioso es que lo que hacía la app PHP de 2006 y lo que hacen las apps Spring Boot actuales no es tan distinto. Al final todo es CRUD; solo que ahora tiene alrededor mucha más decoración y herramientas empresariales.
No arrastras una dependencia distinta por cada punto de una checklist. Puede haber trabajo duplicado, pero la ruta de actualización se vuelve mucho más fácil.
scp, en general eso no es un problema.Por ejemplo, con formatos estándar de mensajes de comunicación como FHIR o HL7, nadie querría implementar por cuenta propia toda la compleja definición del estándar.
Escribir tus propias funciones criptográficas también suele ser pegarte un tiro en el pie, y los problemas graves de seguridad descubiertos durante años lo han demostrado.
Hoy estamos en una época en la que queremos enfocarnos en resolver problemas de negocio más que en cómo se construye correctamente una solución. Con la llegada de la IA, esto se vuelve aún más importante, porque todo el código se siente como si estuviera pegado a ciegas.
Dedicar tiempo a construir todo uno mismo puede ser ventajoso a largo plazo, pero primero hay que sobrevivir a la competencia. Tal vez un competidor ya capturó el mercado al principio con código rápido y desechable.
Hace poco migramos de Java 8, Spring Boot 2 y Swagger a Java 17, Spring Boot 3.3 y OpenAPI 3, y fue bastante painless.
Todavía hay que subir algunas dependencias directas y transitivas, pero el mayor obstáculo se resolvió con la migración.
Pero mientras no enlaces contra una biblioteca C, las dependencias transitivas privadas no importan en absoluto. En el mismo árbol de dependencias puedes tener tantas versiones de un crate incompatibles con SemVer como quieras, e incluso depender directamente de varias versiones si hace falta.
No necesitas actualizaciones masivas al estilo Java; solo subes la dependencia vulnerable. Tengo entendido que C# tiene una funcionalidad parecida, aunque es algo más verbosa.
Estoy 100% de acuerdo. NodeJS tuvo un gran impacto en mi carrera, pero NPM me dejó un trauma mayor que una infancia desastrosa.
Imagina a un programador nuevo que está haciendo una app para mostrársela a sus amigos. Quiere agregar una nueva dependencia, pero no es compatible con otra, así que decide actualizar todo. Al instante nada funciona y Babel empieza a gritar.
Uno piensa que de alguna forma lo resolverá, pero pronto se encuentra con issues abiertos en Git donde literalmente ni siquiera funciona lo básico. Por ejemplo, en Expo hay un issue abierto en el que un proyecto nuevo básico de React Native no compila en Android.
La mitad de las veces parece que a nadie le importa, y en esos casos la solución tampoco está en el ecosistema de Node, sino en algún lugar del ecosistema de Android. Es cinta adhesiva hasta el fondo. Aun así, si un proyecto de miles de millones de dólares puede distribuir una plantilla que no funciona, eso también da confianza para no sentir síndrome del impostor cuando la mitad de mi proyecto paralelo no anda.
Empecé a ver el momento de incorporar la primera dependencia como un fracaso personal. Porque en ese instante, en vez de simplemente cargar JS común con una etiqueta script, de pronto hacía falta todo un sistema de empaquetado y organización solo para usar un
package.json.Fue algo que me sorprendió al pasar de Go al ecosistema de Rust. Un proyecto maduro en Go, por ejemplo el backend web de producción de alguna empresa, puede tener apenas 10 a 20 dependencias, incluyendo dependencias transitivas.
Como dice el texto, incluso un proyecto pequeño en Rust tiene muchas probabilidades de terminar con muchas más, y si se hacen tareas asíncronas, casi está garantizado.
No sé bien si esto se debe a la cultura o a las características del lenguaje. Por ejemplo, en Go las interfaces se satisfacen implícitamente, así que no hace falta traer algo para decir que se está implementando.
En Go hay cosas incluidas en el lenguaje o en la biblioteca estándar que en Rust requieren dependencias: green threads, canales, expresiones regulares, cliente HTTP, servidor HTTP, manejo de tiempo, flags de línea de comandos, logger, lectura/escritura de GIF animados, etc.
No es que me encante el lenguaje Go en sí, pero en herramientas y biblioteca estándar va a la cabeza, y sin duda es lo mejor que he usado.
Estoy muy de acuerdo con la tesis. Permitir que las abstracciones de otras bibliotecas queden expuestas fuera de tu propia biblioteca tiene muchos costos ocultos. No digo que nunca deba hacerse, pero al decidirlo hay que sopesar bien los costos futuros.
Si el paquete que envuelve algo cambia de diseño o de objetivo con frecuencia, aparece inestabilidad. Una funcionalidad que originalmente era la forma más simple de garantía en ingeniería de software desaparece o se fragmenta.
También dificulta que participen en el ecosistema personas propietarias que quizá no sean contribuyentes open source de carrera, pero sí conozcan muy bien un área concreta. Si “hice una forma prolija de hacer X” termina convertido en semanas de discusión, negociación y política sobre que “X debe encajar debajo de Y porque a la gente tal vez le guste Z”, se desperdicia el tiempo de todos.
Algo que aprendí en la vida es que lo más simple es lo que más perdura. Deberíamos elogiar más seguido los minilitos (miniliths) y los monolitos. Tampoco es un problema exclusivo de Rust; lo he visto en varios lenguajes. Muchas veces las comunidades open source empujan con fuerza paquetes de tamaño atómico, y a menudo me pregunto si no responde más a plantar bandera o transferir propiedad que al interés real de la comunidad de usuarios.
En 2025 llegué un poco de casualidad a la idea de que es más rápido pedirle a ChatGPT o Cursor que cree implementaciones sin dependencias de funciones comunes, y cada vez estoy más de acuerdo. Sobre todo después de sufrir el infierno de dependencias de una app grande en React.
También hay que pensar en las dependencias de SaaS o servicios de terceros. Muchas de ellas son patrones comunes y problemas ya resueltos, así que un LLM puede replicarlas rápidamente.
Para problemas de alcance limitado funciona bastante bien, y puedo hacer que la IA produzca una implementación más completa y robusta que la que escribiría yo directamente.
Si más adelante decido agregar una biblioteca, ya existe una encapsulación natural. Solo tengo que cambiar la implementación de mi función para que use la biblioteca, y no necesariamente tocar todos los lugares donde se usa. Cuando llega el momento, también es más fácil probar varias bibliotecas.
Irónicamente, es bastante interesante. Armin es el autor original de Flask, el framework web de Python. En una época similar también existía Bottle, una biblioteca muy parecida.
Tenían casi las mismas funciones, pero Flask se volvió muy popular, mientras que yo siempre preferí Bottle. Como era un solo archivo y no tenía dependencias, era muy fácil copiarlo directamente a un proyecto.
También era fácil modificarlo, y con el tiempo llegué a entenderlo por completo. Para servidor y websockets le agregaba Gevent, pero con ese enfoque se podían construir proyectos bastante pesados.
Incluso hoy tengo un fuerte impulso de usar Bottle para proyectos web pequeños. Lo malo es que no siguió muchas de las prácticas modernas que Python adoptó durante años, así que ahora se ve un poco anticuado.
Para construir cosas por cuenta propia se necesita una capacidad de ingeniería competente. Si solo tienes ingenieros que siempre han tomado bibliotecas de ecosistemas como NPM o PyPI, les va a costar desarrollar por sí mismos soluciones para muchos problemas. Más aún si esas soluciones deben durar y tener la flexibilidad necesaria.
“No programarte a ti mismo hasta un callejón sin salida” requiere mucha práctica.
He visto muchas veces casos en los que es fácil hacerlo mejor que una biblioteca existente. En un proyecto implementé un parser para una variante de Markdown con metadatos al inicio del archivo. Usaba una gramática pequeña, y el parser quedó en menos de una pantalla de código.
Pero la biblioteca de frontend que parseaba el mismo archivo era mucho peor de lo esperado. Se rompía si el identificador de metadatos tenía un guion, y resultó que estaba usando directamente el identificador de metadatos como nombre de miembro de un objeto. En vez de usar un objeto JSON simple, restringía artificialmente la elección de nombres y se rompía si había guiones, como en
something-something.Al final descartaron mi parser, con el argumento de que la gente tendría que “mantenerlo”. Pero simplemente funcionaba, y podía adaptarse fácilmente a cambios en la gramática. Con saber un poco sobre parsers, no tenía nada difícil de entender.
Aunque cueste creerlo, parece que en el equipo nadie salvo yo había escrito un parser con una biblioteca generadora de parsers. Y hay muchos otros ejemplos como este.
Si para usar una sola función terminas compilando cientos, debería prenderse una alarma. Hace aproximadamente un año trabajé en un proyecto para actualizar dependencias de terceros.
Una de ellas era una biblioteca matemática muy completa, con todo tipo de funciones. Al investigar un poco, resultó que lo único que usábamos era un solo método para encontrar la mediana de una lista.
Le señalé al ingeniero responsable la página de Wikipedia, eliminé la dependencia y le pedí que escribiera un único método que realizara esa operación matemática.
Pero el verdadero problema no es usar dependencias de terceros en sí, sino que necesitamos el concepto de traer solo una parte acotada de una biblioteca. Si solo necesitas una pequeña porción de una biblioteca enorme, ¿por qué traerla completa? He oído propuestas de “microframeworks” como una forma de resolver esto.
Lamentablemente, no parece que la gente revise con cuidado el conjunto de features predeterminadas que usan sus dependencias ni que las reduzca activamente. La sintaxis para traer features adicionales es simple, pero para quitar features opcionales activadas por defecto hay que especificar
no-default-featuresy aun así volver a agregar una por una las features predeterminadas que sí se quieren, lo cual es más engorroso.Peor aún: para que una biblioteca permita recortar features innecesarias de sus propias dependencias, tiene que crear features propias y mapearlas a las features de cada dependencia. Por ejemplo, si tienes 5 dependencias y cada una tiene 1 dependencia obligatoria y 4 opcionales, para darle al usuario control total sobre las features transitivas tendrías que crear 20 features de mapeo en tu propia biblioteca. Y eso ni siquiera incluye las features que crearías para cuidar a los usuarios aguas abajo de tu propio código.
Cada vez veo más que la usabilidad alrededor de las features, en especial para reducir el bloat innecesario, es tan mala que termina siendo un catalizador de los problemas de tiempos de compilación en Rust. No parece que este tema se discuta mucho cuando surge, así que quizá debería escribir una entrada de blog con mi postura fuerte para poder señalarla si el estado actual continúa.
Si el sistema de confianza es bueno, por un lado no creo que sea un problema.
También me gusta hasta cierto punto el enfoque tipo Shadcn: si te gusta un componente, lo copias a tu propia biblioteca. Pero si aparece una vulnerabilidad, no puedes saber si te afecta.
Para cierto código está bien, pero en otros casos realmente debes depender de que lo revise la mayor cantidad de ojos posible.
Median(list) { let len = length(list) if len % 2 == 0 { let x = floor(len/2) return (list[x] + list[x+1]) / 2 } return list[len/2] }Eso sí, asume que la lista ya está ordenada. Resistí la tentación de llamar a
is_oddaquí.