- Las dependencias parecen funcionalidades gratuitas, pero en realidad traen diversos costos y complejidad
- Una dependencia equivocada puede generar varios riesgos, como curva de aprendizaje, cambios repentinos de interfaz y problemas de despliegue e instalación
- Como caso representativo, TigerBeetle apunta a una política de "cero dependencias" por seguridad, rendimiento y simplicidad operativa
- El autor propone un marco de evaluación de dependencias (ubicuidad, estabilidad, profundidad, ergonomía y hermeticidad)
- Es indispensable pensar críticamente para distinguir entre buenas y malas dependencias y, al elegirlas, considerar no solo la productividad a corto plazo sino también el costo total y el riesgo
Una dependencia equivocada cuesta más que NIH (Not Invented Here)
Desventajas y costos ocultos de las dependencias
- Muchos desarrolladores ven agregar una dependencia como una "funcionalidad gratis", pero en la práctica aparecen costos como los siguientes
- Tiempo y complejidad para aprenderla
- Con frecuencia, incluso instalar la dependencia puede ser difícil
- Al actualizar a una versión mayor, también surge la carga de modificar bastante tu propio código
- La dependencia finalmente debe estar presente en el entorno de despliegue/instalación (contenedores, bundling, etc.), lo que añade complejidad
- Introducir una mala dependencia puede terminar creando una estructura de despliegue compleja que no tiene relación con la funcionalidad principal
El principio de cero dependencias de TigerBeetle
- TigerBeetle es una base de datos financiera basada en Vanilla Zig y adopta una política de cero dependencias
- Se desarrolla únicamente con el lenguaje Zig y no tiene dependencias externas aparte del toolchain de Zig
- El objetivo es evitar problemas como riesgo de ataques a la cadena de suministro, degradación del rendimiento y mayor tiempo de instalación causados por dependencias
- Cuanto más profundamente arraigado esté un software en la infraestructura, más se amplifican en toda la pila los costos derivados de las dependencias
- Usar una caja de herramientas pequeña y estandarizada favorece el mantenimiento y la productividad de desarrollo
- Se enfoca en resolver problemas nuevos rápidamente y reducir la complejidad usando solo Zig
Marco de evaluación de dependencias
- Aunque reconoce que para cualquier desarrollador es imposible no tener ninguna dependencia, se enfatiza que las dependencias deben evaluarse con cuidado
- El autor propone evaluar las dependencias con los siguientes 5 criterios
- Ubicuidad (Ubiquity): ¿Qué tan común es en el entorno objetivo? ¿Requiere despliegue/instalación adicional?
- Estabilidad (Stability): Compatibilidad hacia atrás, frecuencia de cambios de API, descontinuación y otros problemas
- Profundidad (Depth): Cuánta funcionalidad real oculta detrás del API y qué tan difícil sería reemplazarla implementándola directamente
- Ergonomía (Ergonomics): Si el API es intuitivo/declarativo y fácil de usar, y cómo está la documentación
- Hermeticidad (Watertightness): Qué tan bien funciona la abstracción y cuánto puedes dejar de preocuparte por la tecnología subyacente
- En la comunidad de desarrollo normalmente solo se discute la usabilidad, mientras que las otras cuatro suelen pasarse por alto
Ejemplos de buenas dependencias
-
Llamadas al sistema POSIX
- Universalidad: se pueden usar en casi todas las plataformas, como Linux, Android, macOS y BSD
- Estabilidad: la compatibilidad de la interfaz es muy alta y casi no cambia
- Profundidad: una sola API oculta cientos de miles de líneas de código del kernel
- Ergonomía: aunque tiene un estilo algo tradicional de C, no presenta grandes dificultades de uso
- Integridad: en general no hay mayores problemas, aunque existen detalles como la persistencia de datos en dispositivos de almacenamiento
-
Códigos de control de terminal ECMA-48
- Universalidad: la mayoría de las terminales los soportan, excepto
cmd.exe de Windows
- Estabilidad: no han cambiado desde 1991
- Profundidad: crear el estándar por cuenta propia sería absurdamente difícil
- Ergonomía: salvo por lo críptico que resulta el carácter Esc, son razonables
- Integridad: hay muy poca preocupación por dependencias de hardware
-
Plataforma web (Web API, HTML, JS, CSS, etc.)
- Universalidad: los navegadores web están instalados en casi todos los entornos del mundo
- Estabilidad: políticas muy fuertes de compatibilidad hacia atrás
- Profundidad: construir un navegador propio es algo tan profundo que en la práctica resulta imposible
- Ergonomía: tiene algo de complejidad, pero la documentación y las herramientas de desarrollo son excelentes
- Integridad: salvo casos especiales como archivos, audio y video, tiene un grado de integridad muy alto
Conclusión
- En lugar de copy-paste y abuso de dependencias, hacen falta pensamiento crítico y un análisis del costo total
- Al introducir dependencias, hay que evaluar críticamente los costos y beneficios de cada una y
considerar también los riesgos y costos potenciales del sistema completo, para elegir con cuidado; a largo plazo, eso resulta mucho más barato y seguro
2 comentarios
TigerBeetle: base de datos OLTP especializada en contabilidad
Opinión de Hacker News
Era la primera vez que escuchaba el ejemplo de TigerBeetle, así que lo busqué por mi cuenta. Si estuvieras construyendo una base de datos de libro mayor financiero basada en Zig, donde la seguridad y el rendimiento son críticos, y necesitas soportar un millón de transacciones por segundo en un solo núcleo de CPU, entonces evitar agregar dependencias por el riesgo que implican es totalmente razonable. Pero la mayoría de los desarrolladores comunes están construyendo sistemas CRUD bastante normales, y por lo general no tienen ese nivel de exigencia. Muchas dependencias pueden estar llenas de bugs, pero en muchos casos aun así tienen mejor calidad que lo que construiría la mayoría de los desarrolladores por su cuenta. En la práctica, las empresas también terminan limitadas por el nivel de los desarrolladores que pueden contratar. Cada situación es distinta, así que hay que recordar que incluso consejos opuestos pueden ser correctos dentro de su propio contexto
De hecho trabajo en TigerBeetle. La clave es el contexto. TigerBeetle y rust-analyzer tienen una cultura de desarrollo fuerte, pero resuelven problemas distintos y por eso formaron culturas diferentes. Las dependencias mencionadas en el artículo, como POSIX, ECMA-48 y la plataforma web, son más bien interfaces de sistema que bibliotecas. Si una dependencia de biblioteca da problemas, simplemente la puedes reescribir tú mismo, pero algo fundamental como una interfaz de sistema es prácticamente imposible o muy costoso de cambiar. Es poderoso tomar la decisión de no hacer cosas que no encajan con el alcance del software. Por ejemplo, si tuvieras un equipo especializado en crear código de multiplicación de matrices, podrías usar bibliotecas externas para otras cosas que no son parte del trabajo central, pero creo que sería mejor diseñar mejor la descomposición de responsabilidades del producto. Así se pueden aislar mejor dentro del sistema las dependencias esenciales
El problema de esta postura es que solo se sostiene si piensas únicamente en desarrolladores de nivel muy bajo. En el sector tecnológico hay una tendencia a diluir el consejo hasta adaptarlo al nivel más bajo, pero honestamente, en casi ningún lugar donde he trabajado los desarrolladores eran tan débiles como para no poder resolver un problema forkeando una dependencia rota. También hay mucho desprecio hacia CRUD, pero una mala abstracción puede generar una enorme pérdida de tiempo y muchísimo sufrimiento en un sistema CRUD. Incluso las opciones populares tienen muchos problemas y baja productividad una vez que sales del nivel de tutorial más básico
No tiene que ver con el nivel de los desarrolladores en sí. Uses el toolchain o producto que uses, al final siempre estás usando dependencias hechas por otros. Conozco poquísima gente que implemente por su cuenta código de multiplicación de matrices, y ni siquiera ellos implementan también todas las bibliotecas open source que no tienen que ver con su propio trabajo. Normalmente la gente se obsesiona con las dependencias por requisitos regulatorios, interés personal o apego a cierta biblioteca específica. Si todos aplicáramos ese principio al extremo, estaríamos viviendo en la playa recogiendo arena
La idea de que “los desarrolladores CRUD promedio no son fuertes” es demasiado tajante. La mayoría de los desarrolladores no puede elegir los sistemas sobre los que trabaja, y durante el desarrollo siempre faltan recursos. Hay que aprovechar dependencias “baratas” para poder lanzar rápido software que funcione. Parece un comentario hecho sin entender bien esa realidad
TigerBeetle es una startup relativamente reciente y todavía ni siquiera está en una versión 1.0 estable. Me parece demasiado pronto para decir si este enfoque realmente funciona
El NIH (Not Invented Here) en sí puede ser muy útil cuando se usa evaluando de forma realista hasta dónde vas a hacerte responsable. Por ejemplo, un framework web frontend que encaje exactamente con mi dominio muchas veces sí vale la pena crearlo y mantenerlo por cuenta propia. Pero con bases de datos, motores de juego, servidores web o funciones básicas relacionadas con criptografía, la historia es distinta. Si el problema es tan difícil que no se puede resolver con soluciones existentes, antes habría que pensar en redefinir el problema. Creo que es mucho más barato replantear el problema mismo que reconstruir desde cero todo el set de pruebas de SQLite
También hay muchos casos donde construir tú mismo una base de datos, un motor de juego, un servidor web o primitivas criptográficas termina siendo mejor. Si con archivos simples e índices en tiempo de ejecución es suficiente, muchas veces hasta SQLite resulta excesivo. En muchos juegos, sobre todo con equipos pequeños, un motor personalizado puede convenir más. La ventaja de un motor ya hecho es el pipeline, pero viene con una sobrecarga importante. Un servidor web no tiene más complejidad que una app FastCGI. Y en criptografía, no todo escenario es un problema de seguridad; para algo como verificar un hash simple, implementarlo uno mismo puede ser aceptable. No me parece bueno desarrollar indefensión aprendida frente a temas que se ven difíciles. También es importante recordar que, aunque una solución existente resuelva el problema, eso no significa que sea la forma óptima o más eficiente de hacerlo
Entonces, ¿por qué existen tantos motores de base de datos? Porque al final los sistemas informáticos complejos siempre tienen trade-offs distintos. Hay muchísimas variables: restricciones, escalabilidad, concurrencia, seguridad, características de los datos, forma de almacenamiento, etc. Yo tiendo a confiar más en una dependencia cara cuando esa dependencia, a su vez, busca minimizar sus propias dependencias. Los sistemas automatizados de gestión de dependencias suelen complicar más las cosas, así que una gestión manual cuidadosa puede reducir la carga
Creo que hay dos razones para usar dependencias de terceros. (1) Cuando las publica directamente el proveedor del servicio y su ciclo de vida coincide relativamente con el mío. (2) Cuando reemplazan código complejo que yo no quiero escribir. El caso (1) no tiene problema porque responde a una razón de negocio, aunque hay que aceptar que cuando ese servicio se actualice puede haber cambios grandes. El caso (2) vale más o menos según la complejidad del código que uno está evitando escribir. Introducir una dependencia significa gastar tiempo y recursos en actualizar y probar al ritmo de otra persona, y hacerse cargo de esa responsabilidad
Uno se topa bastante rápido con problemas que no se pueden resolver bien con un RDBMS. Los RDBMS están diseñados alrededor de modificaciones concurrentes de datos y soporte de datasets mutables, así que si no necesitas eso, un índice simple puede darte mejoras enormes de rendimiento. Si los datos son inmutables, puedes hacer una implementación propia muchísimo más rápida que un RDBMS
El caso de los RDBMS es interesante. En Wikipedia hay más de 100 RDBMS, y cada uno tiene problemas que puede resolver y otros que no. Son resultados de pensar en soluciones prácticas y llevarlas realmente a la ejecución
Las dependencias introducen riesgos, pero no usarlas en absoluto puede hacerte perder competitividad en desarrollo y salida al mercado. Por eso es importante un proceso de gestión de dependencias
Los puntos 4 y 5 son realmente importantes, pero se olvidan seguido. Incluso en proyectos personales, cuando vuelves después de bastante tiempo, a veces te encuentras con dependencias obsoletas o repos borrados. Por eso ahora suelo hacer forks privados del código fuente y compilarlo yo mismo, y también forkeo a nivel de código fuente hasta las dependencias de mis dependencias. Así, aunque el ecosistema cambie de golpe más adelante, se minimiza el daño. He terminado prefiriendo bibliotecas en código fuente antes que binarios
En el punto 5, hacer fork puede ser una carga excesiva. Otra opción razonable es vendorizar las dependencias en tu propio git o en un proxy de caché. Encaja especialmente bien en proyectos de larga vida. Cuando hay muchos archivos de dependencias, como en NodeJS, herramientas como Yarn o PNPM pueden ser más eficientes
Respecto al punto 4, una dependencia conocida como SQLite va a durar muchísimo más que el producto que yo construya. Pensar que mi producto va a sobrevivir más que ese proyecto open source sería una postura más arrogante. Tampoco pienso compilar yo mismo el kernel de Linux
Todo el código debería poder compilarse como mínimo sin conexión de red. Lo ideal es no tener artefactos binarios, aunque no siempre es realista
Es una gran observación. Voy a agregar esto a mi documento de proceso para introducir dependencias. El problema son casos como JavaScript, donde el árbol de dependencias es demasiado profundo
Después de muchos años, mi experiencia es que al final todo se reduce a “depende” (It Depends™). Cuando era más joven, me obsesionaba con seguir principios sin excepciones y terminé produciendo código pésimo por forzar bibliotecas o paradigmas inadecuados. Ahora convierto en bibliotecas propias las cosas que uso seguido y las saco como paquetes, y cubro con dependencias externas solo aquello que necesito y no sé hacer. Si la calidad o la mantenibilidad me convencen, acepto con gusto también lo externo
En la industria energética hay una tendencia deliberada a evitar dependencias. Si introduces una dependencia externa, tienes que revisar todos sus cambios. Las herramientas de código con IA han ayudado mucho, y se usan sobre todo para generar herramientas CLI. También hacemos documentación OpenAPI con LLM y luego la servimos con la biblioteca estándar de Go. El LLM en sí es una dependencia externa, pero la herramienta CLI creada con eso no está vinculada al código real, así que sus exigencias de calidad son menores. Claro, los desarrolladores frontend no van a querer trabajar sin React, pero esos productos los tratamos aparte, así que es una excepción. Si les das a los ingenieros pequeñas herramientas que reduzcan su obsesión por depender de la calidad de terceros, se vuelve más fácil aplicar una política de minimizar dependencias
Saber distinguir entre dependencias buenas y malas es una habilidad importante. Personalmente creo que las dependencias pagas suelen salir perdiendo. Hay muchas probabilidades de que la empresa que las ofrece las haya diseñado para inducir lock-in. El “minimalismo de dependencias” me parece un muy buen concepto (tuit relacionado de VitalikButerin)
Las dependencias pagas tienen soporte en un solo lugar, así que si la empresa proveedora cierra, todo el proyecto queda en riesgo. Como casi ninguna empresa dura para siempre, hay que evaluar si el futuro de esa dependencia puede afectar la trayectoria de tu proyecto
He tenido malas experiencias con dependencias pagas impuestas por equipos sin perfil técnico. En cambio, dependencias “open core” ampliamente usadas por la comunidad, como Sidekiq, me parecen bastante más confiables porque es mucho menos probable que desaparezcan de la nada. La ventaja de lo pago es que, mientras la empresa esté sana, no tienes que preocuparte por el soporte
Tanto los componentes pagos como los gratuitos ofrecidos por empresas tienen vendor lock-in. Gestionar ese riesgo le toca al equipo de integración, que tiene que buscar alternativas o modularizar para controlarlo
Si es una dependencia paga, debería entregarse obligatoriamente sobre una interfaz basada en estándares abiertos o con implementaciones alternativas, para evitar el lock-in. Si existen otras opciones, conservas la posibilidad de cambiar
Que una dependencia paga parezca mala puede ser señal de que no hay suficiente presupuesto. Yo quiero que dar soporte a mi código sea el “trabajo” de alguien. Si hay muchos candidatos o muchos desarrolladores dispuestos a hacerse responsables por voluntad propia, eso da estabilidad; y aun en un proyecto personal, aunque el código sea abierto, necesitas a alguien que te apoye si surge un problema. Lo importante es que haya sentido de responsabilidad para no abandonar todo si la empresa se retira
Mucha gente tiende a obsesionarse con escribir código nuevo, pero en la práctica hasta una dependencia malísima suele ser una opción mucho más eficiente en 9 de cada 10 casos
Las dependencias son un arma de doble filo. En las grandes empresas de software, muchas veces sale más barato abandonar el mantenimiento del código y reescribirlo. En agencias pequeñas de web o branding, prácticamente no hace falta un backend de gran calidad. En cambio, los temidos patrones enterprise surgieron precisamente para permitir aislar y mantener código que pueda preservarse sin dependencias externas, incluso cuando dentro de 5 años ya no existan ni la documentación ni la memoria del sector. Las dependencias externas implican dos riesgos: que se dejen de soportar o que introduzcan cambios destructivos. Al final eso afecta el flujo de desarrollo de funcionalidades. Si el componente es interno, esos trade-offs también se pueden controlar internamente. Si haces SaaS, usar dependencias rápido para triunfar a corto plazo probablemente sea correcto; si necesitas seguridad y soporte a largo plazo, hay que pensar más a futuro para tener éxito. Casi nunca escribir código nuevo es el verdadero cuello de botella de una organización
Me pregunto qué tan en serio manejan en las empresas las vulnerabilidades de seguridad y las licencias. Antes yo era más permisivo con las dependencias, pero desde que me cambié a una empresa estricta con seguridad y licencias, mi perspectiva cambió muchísimo
También es importante la diferencia entre bibliotecas y frameworks. Una biblioteca es una herramienta que hace bien una sola cosa; un framework define toda la estructura de la app. La comunidad de Go tiende a mantenerse lejos de frameworks grandes y prefiere la biblioteca estándar, bibliotecas ligeras y, si hace falta, copiar y pegar código fuente. Por ejemplo, frameworks como Gin (web API) o GORM (ORM) son cómodos de usar, pero limitan la estructura interna y agregan complejidad. Como el SDK estándar de Go ya es bastante sólido, creo que lo correcto es no agregar dependencias más allá de lo necesario
El autor es de Nueva Zelanda. De fondo está la mentalidad neozelandesa del Number 8 wire, es decir, la actitud de “resolver como sea con lo que hay y con ingenio” (artículo de wiki sobre Number 8 wire)
Muchos desarrolladores con experiencia de países distintos de Nueva Zelanda también suelen estar de acuerdo con algo parecido. Casi todos han sufrido por elegir mal una dependencia o por actualizar una biblioteca sin necesidad
Como alguien de Nueva Zelanda, siento que la mentalidad de Number 8 Wire ya había desaparecido hace 20 años
Es la primera vez que me entero de eso hoy. Me hace pensar en la expresión australiana “She'll buff out, mate”
El tamaño y la base comercial de una dependencia también afectan su escalabilidad. Si una herramienta ya se usa en despliegues 100 a 1000 veces mayores que los míos, es poco probable que choque contra sus límites en mi caso. A esa escala también es más probable que los bugs se descubran o se corrijan antes, y por eso a mí me termina funcionando de forma más segura
Incluso bibliotecas grandes a veces no funcionan para nada en entornos pequeños. Por ejemplo, el compilador de Protocol Buffers para Swift antes se caía con campos inesperados. Muchas empresas grandes tampoco prueban realmente esas rutas si no están ligadas a sus objetivos a gran escala
He encontrado bugs graves en bibliotecas famosas creadas por empresas grandes (Meta, Google, Microsoft, etc.). Aunque reportes el issue, tardan mucho en arreglarlo y además se resisten bastante al cambio. En esas situaciones, al final implementarlo yo mismo fue más rápido e incluso mejoró el rendimiento. En consultoría, especialmente, la dirección del trabajo suele sacudirse por pedidos poco razonables del cliente. A medida que crece mi confianza como desarrollador en que puedo hacerlo por mi cuenta, cada vez veo más casos donde me conviene implementarlo yo mismo en vez de depender de algo externo y gigantesco. Claro, no haría un navegador o un modelo de IA, pero sí he implementado cosas como un motor de inferencia local, un renderizador HTML o una base de datos de grafos propia. Lo que esperan los clientes no es novedad o innovación, sino reducción de riesgo. Si lo desarrollo yo mismo, es mucho más fácil cumplir los plazos. Me resulta más eficiente construirlo que pasar horas revisando documentación de Google o de otras grandes empresas. Últimamente he estado trabajando 12 horas diarias durante 3 meses para rescatar un proyecto que el equipo anterior había dejado abandonado, así que estas ideas me han estado rondando bastante de madrugada