Aprendes comparando con lo que ya sabes. Recientemente me llevé una sorpresa al asumir que Rust funcionaba como Java respecto a la resolución de versiones de dependencias transitivas. En esta publicación, quiero comparar los dos.
Antes de profundizar en los detalles de cada stack, describamos el dominio y los problemas que conlleva.
\ Al desarrollar cualquier proyecto por encima del nivel Hello World, es probable que te enfrentes a problemas que otros han enfrentado antes. Si el problema está muy extendido, la probabilidad es alta de que alguien haya sido lo suficientemente amable y cívico como para haber empaquetado el código que lo resuelve, para que otros lo reutilicen. Ahora, puedes usar el paquete y concentrarte en resolver tu problema principal. Así es como la industria construye la mayoría de los proyectos hoy en día, incluso si trae otros problemas: te sientas sobre los hombros de gigantes.
\ Los lenguajes vienen con herramientas de construcción que pueden agregar dichos paquetes a tu proyecto. La mayoría de ellos se refieren a los paquetes que agregas a tu proyecto como dependencias. A su vez, las dependencias de los proyectos pueden tener sus propias dependencias: estas últimas se denominan dependencias transitivas.

En el diagrama anterior, C y D son dependencias transitivas.
\ Las dependencias transitivas tienen problemas por sí mismas. El más grande es cuando se requiere una dependencia transitiva desde diferentes rutas, pero en diferentes versiones. En el diagrama a continuación, A y B dependen de C, pero en diferentes versiones.

¿Qué versión de C debería incluir la herramienta de construcción en tu proyecto? Java y Rust tienen respuestas diferentes. Describámoslas por turnos.
Recordatorio: el código Java se compila a bytecode, que luego se interpreta en tiempo de ejecución (y a veces se compila a código nativo, pero esto está fuera de nuestro espacio de problemas actual). Primero describiré la resolución de dependencias en tiempo de ejecución y la resolución de dependencias en tiempo de compilación.
\ En tiempo de ejecución, la Máquina Virtual de Java ofrece el concepto de classpath. Cuando tiene que cargar una clase, el tiempo de ejecución busca a través del classpath configurado en orden. Imagina la siguiente clase:
public static Main { public static void main(String[] args) { Class.forName("ch.frankel.Dep"); } }
\ Vamos a compilarla y ejecutarla:
java -cp ./foo.jar:./bar.jar Main
\ Lo anterior primero buscará en el foo.jar la clase ch.frankel.Dep. Si la encuentra, se detiene allí y carga la clase, independientemente de si también podría estar presente en el bar.jar; si no, busca más en la clase bar.jar. Si aún no se encuentra, falla con un ClassNotFoundException.
\ El mecanismo de resolución de dependencias en tiempo de ejecución de Java es ordenado y tiene una granularidad por clase. Se aplica tanto si ejecutas una clase Java y defines el classpath en la línea de comandos como se muestra arriba, o si ejecutas un JAR que define el classpath en su manifiesto.
\ Cambiemos el código anterior a lo siguiente:
public static Main { public static void main(String[] args) { var dep = new ch.frankel.Dep(); } }
\ Debido a que el nuevo código hace referencia a Dep directamente, el nuevo código requiere resolución de clases en tiempo de compilación. La resolución del classpath funciona de la misma manera:
javac -cp ./foo.jar:./bar.jar Main
\ El compilador busca Dep en foo.jar, luego en bar.jar si no lo encuentra. Lo anterior es lo que aprendes al comienzo de tu viaje de aprendizaje de Java.
\ Después, tu unidad de trabajo es el Java Archive, conocido como JAR, en lugar de la clase. Un JAR es un archivo ZIP glorificado, con un manifiesto interno que especifica su versión.
\ Ahora, imagina que eres un usuario de foo.jar. Los desarrolladores de foo.jar establecieron un classpath específico al compilar, posiblemente incluyendo otros JARs. Necesitarás esta información para ejecutar tu propio comando. ¿Cómo pasa un desarrollador de biblioteca este conocimiento a los usuarios posteriores?
\ La comunidad propuso algunas ideas para responder a esta pregunta: La primera respuesta que se mantuvo fue Maven. Maven tiene el concepto de Project Object Model, donde estableces los metadatos de tu proyecto, así como las dependencias. Maven puede resolver fácilmente dependencias transitivas porque también publican su POM, con sus propias dependencias. Por lo tanto, Maven puede rastrear las dependencias de cada dependencia hasta las dependencias hoja.
\ Ahora, volviendo al problema: ¿cómo resuelve Maven los conflictos de versiones? ¿Qué versión de dependencia resolverá Maven para C, 1.0 o 2.0?
\ La documentación es clara: la más cercana.

En el diagrama anterior, la ruta a v1 tiene una distancia de dos, una a B, luego una a C; mientras tanto, la ruta a v2 tiene una distancia de tres, una a A, luego una a D, y finalmente una a C. Por lo tanto, la ruta más corta apunta a v1.
\ Sin embargo, en el diagrama inicial, ambas versiones de C están a la misma distancia del artefacto raíz. La documentación no proporciona respuesta. Si estás interesado en ello, ¡depende del orden de declaración de A y B en el POM! En resumen, Maven devuelve una sola versión de una dependencia duplicada para incluirla en el classpath de compilación.
\ Si A puede trabajar con C v2.0 o B con C 1.0, ¡genial! Si no, probablemente necesitarás actualizar tu versión de A o degradar tu versión de B, para que la versión resuelta de C funcione con ambos. Es un proceso manual que es doloroso, pregúntame cómo lo sé. Peor aún, podrías descubrir que no hay una versión de C que funcione con A y B. Es hora de reemplazar A o B.
Rust difiere de Java en varios aspectos, pero creo que los siguientes son los más relevantes para nuestra discusión:
\ Examinémoslos uno por uno.
\ Java compila a bytecode, luego ejecutas este último. Necesitas establecer el classpath tanto en tiempo de compilación como en tiempo de ejecución. Compilar con un classpath específico y ejecutar con uno diferente puede llevar a errores. Por ejemplo, imagina que compilas con una clase de la que dependes, pero la clase está ausente en tiempo de ejecución. O alternativamente, está presente, pero en una versión incompatible.
\ Contrario a este enfoque modular, Rust compila en un paquete nativo único el código del crate y cada dependencia. Además, Rust proporciona su propia herramienta de construcción, evitando así tener que recordar las peculiaridades de diferentes herramientas. Mencioné Maven, pero otras herramientas de construcción probablemente tienen reglas diferentes para resolver la versión en el caso de uso anterior.
\ Finalmente, Java resuelve dependencias desde binarios: JARs. Por el contrario, Rust resuelve dependencias desde fuentes. En tiempo de compilación, Cargo resuelve todo el árbol de dependencias, descarga todas las fuentes requeridas y las compila en el orden correcto.
\ Con esto en mente, ¿cómo resuelve Rust la versión de la dependencia C en el problema inicial? La respuesta puede parecer extraña si vienes de un entorno Java, pero Rust incluye ambas. De hecho, en el diagrama anterior, Rust compilará A con C v1.0 y compilará B con C v2.0. Problema resuelto.
Los lenguajes JVM, y Java en particular, ofrecen tanto un classpath en tiempo de compilación como un classpath en tiempo de ejecución. Permite modularidad y reutilización, pero abre la puerta a problemas relacionados con la resolución del classpath. Por otro lado, Rust construye tu crate en un único binario autónomo, ya sea una biblioteca o un ejecutable.
\ Para profundizar más:
Publicado originalmente en A Java Geek el 14 de septiembre de 2025

![[EN DIRECTO] Actualización del mercado: Bitcoin supera los $115,000, Ethereum sube un 6% mientras el mercado de criptomonedas experimenta amplias ganancias](https://static.coinstats.app/news/source/1716914275457.png)
