lunes, 25 de mayo de 2026

Productividad Ilusoria: ¿Optimización real o desplazamiento de carga cognitiva?


 

Siguiendo con una serie de reflexiones sobre el escenario actual del desarrollo de software, comparto una de las ideas más controversiales del momento. Día a día se generan cientos de publicaciones similares a la presente, con opiniones y puntos de vista diversos, opuestos, ¿irreconciliables? La productividad del desarrollador de software está bajo la lupa de toda la industria. Pero hay lugar todavía para preguntarnos si la aceleración en los tiempos de delivery es tan real como aparenta.

Disclaimer: todas las reflexiones expuestas son propias. No es postura oficial de ninguna empresa, empleador, universidad, etc. a la que esté relacionado

Prolegómenos: Epistemología del desarrollo asistido y el sesgo de velocidad

El análisis de los sistemas de producción de software tradicionalmente ha medido la eficiencia a través del rendimiento pseudo-tangible de líneas de código o la velocidad de resolución de tickets (issues, tareas). Sin embargo, la introducción de la inteligencia artificial generativa en esta ecuación altera la relación ontológica entre el ingeniero y el código. No podemos evaluar la productividad sin antes cuestionar si la celeridad en la generación de artefactos de software equivale a una optimización real del proceso de desarrollo o si, por el contrario, estamos ante un sesgo que confunde la alta tasa de conversión de texto a código fuente con eficiencia sustancial.

En la narrativa contemporánea, se acepta de forma casi axiomática que la GenAI ha incrementado la productividad de manera exponencial. Sin embargo, desde una perspectiva de análisis de sistemas y procesos, es posible que este incremento sea, en gran medida, una ilusión en la percepción sensorial del desarrollador.

1. La Ley de Conservación del Esfuerzo

Durante el ciclo de vida del desarrollo, el esfuerzo cognitivo no se elimina; se redistribuye. El tiempo que tradicionalmente se empleaba en la síntesis del código fuente ahora se fragmenta en:

  • Ingeniería de Prompts Multimodal: La formulación de instrucciones con una precisión semántica extrema, junto con la disposición de artefactos auxiliares para mitigar alucinaciones del modelo.

  • El espejismo del refinamiento iterativo: Un ciclo de retroalimentación donde el desarrollador entra en un bucle de ensayo y error hasta lograr la funcionalidad esperada, seguido por iteraciones abocadas a la corrección estructural y estilística que se preconoce como aceptable según el estándar adherido. Esta actividad incesante genera una falsa percepción de progreso, ignorando que el tiempo neto de resolución suele equipararse al desarrollo manual razonado.

2. "Cognitive Offloading" y el Eclipse del Juicio a Priori

La delegación de la lógica a agentes externos produce un fenómeno de Cognitive Offloading (descarga cognitiva). El ingeniero transita de un Juicio a Priori (el diseño arquitectónico y la resolución mental previa) hacia un Juicio a Posteriori (el análisis reactivo de lo que la IA propone), como se expuso en la primera publicación de esta serie.

Este cambio de paradigma se correlaciona directamente con un incremento sustancial en la densidad de Code Smells y deuda técnica latente, tal como se detalla en el estudio "Debt behind the AI Boom". El resultado, entonces, es una sobrecarga en el esfuerzo de mantenimiento que previamente no existía y atenta directamente contra la productividad en términos absolutos.

3. Asimetría en la Asertividad Crítica

Se evidencia, no obstante, un salto de eficiencia genuina en procesos de baja asertividad crítica. En tareas de documentación técnica y gestión ontológica del conocimiento —donde el margen de error no compromete la integridad del sistema— la IA actúa como un catalizador real, liberando al experto de tareas de menor densidad intelectual.

4. La Convergencia de Seniority y la Erosión del Mercado Junior

La GenAI ha logrado un efecto de "nivelación" en la curva de aprendizaje, permitiendo que perfiles Junior realicen entregas con una apariencia de madurez técnica propia de seniorities superiores. No obstante, esta democratización conlleva una externalidad negativa: una posible contracción en la demanda de talento emergente, elevando las barreras de entrada al mercado laboral.

(Nota: Este fenómeno de erosión profesional será objeto de un análisis exhaustivo en mi próxima publicación).

Conclusión

En última instancia, la Inteligencia Artificial Generativa no debe ser simplificada bajo una narrativa de soluciones "mágicas"; no representa un atajo automatizado hacia la excelencia ingenieril, sino que opera como un amplificador de alta sensibilidad. Su integración no exime al sistema de la necesidad de arquitecturas rigurosas, sino que la intensifica.

Para capitalizar su verdadero potencial y extraer un valor sustancial sin comprometer la integridad del software, se requiere la convergencia de dos factores críticos: procesos operativos altamente estandarizados y una maestría técnica avanzada por parte del desarrollador. El dominio de estas herramientas no radica en la delegación ciega de la lógica, sino en la capacidad de orquestar, auditar y gobernar el artefacto generado. Sin este sustrato de madurez metodológica, el incremento de velocidad en las fases tempranas se pagará inevitablemente con entropía y obsolescencia en el ciclo de mantenimiento.

Mientras que la celeridad es una métrica de vanidad; la robustez y la gobernanza del código siguen siendo los vectores reales de la productividad.

Referencias:

#ComputerScience #SoftwareEngineering #GenAI #CognitiveOffloading #TechnicalDebt 

lunes, 11 de mayo de 2026

El eclipse del juicio a priori: Atrofia de la abstracción y relajo cognitivo en la ingeniería de software asistida por modelos de lenguaje

Como desarrolladores, siempre hemos buscado herramientas que optimicen nuestro flujo de trabajo. Sin embargo, la integración masiva de la IA generativa no es solo un cambio de herramienta; es un cambio de paradigma cognitivo.

Últimamente he estado reflexionando sobre cómo esta tecnología está alterando nuestra forma de abordar problemas complejos. 

¿Qué sucede cuando solo activamos nuestro análisis una vez que la propuesta ya está sobre la mesa? ¿Nos convertimos en editores de soluciones ajenas en lugar de los arquitectos de las propias?

Este post es el primero de una serie de reflexiones sobre los nuevos flujos de trabajo en nuestra industria,  las ganancias en productividad, los costos invisibles. En otras palabras, los riesgos, los desafíos y las nuevas oportunidades.


Prolegómenos: La mutación del proceso heurístico

La integración de la Inteligencia Artificial Generativa en el ciclo de vida del desarrollo de software ha introducido una mutación en la heurística del desarrollador. Este fenómeno, caracterizado por un desplazamiento del análisis sistémico hacia la validación puntual, amenaza la integridad arquitectónica de los sistemas complejos.

1. El desplazamiento hacia el juicio a posteriori

 Desde una perspectiva kantiana, el diseño de software tradicional ha dependido fundamentalmente del juicio a priori: la capacidad del ingeniero para estructurar esquemas mentales, modelar los datos para definir la información, establecer contratos de interfaces y prever potenciales colisiones internas del sistema antes de su codificación formal.

Este juicio sintético fue siempre mediador entre el requerimiento provisto y el código fuente producido, actuando como canalizador de esfuerzos. El tiempo invertido para generar el marco teórico dentro del cual el código fuente posee su propia vida fue el espacio donde el desarrollador más valor aportaba al sistema, tomando las decisiones de diseño más profundas. En este esquema, la escritura del propio código era una tarea supeditada a la mecánica y a la técnica más que al pensamiento creativo-arquitectónico.

En cambio, la irrupción de la IA en forma de disponibilidad inmediata de propuestas sintácticas induce un relajo cognitivo. El desarrollador ya no genera la solución; reacciona a ella. Esta transición hacia un juicio exclusivamente a posteriori —donde la crítica solo se activa frente al artefacto ya producido— elimina la fase de reflexión pura.

2. La trampa de la concreción y la pérdida del Big Picture 

Los modelos de lenguaje actuales son, por definición, optimizadores de probabilidad local. Su salida tiende hacia la concreción extrema: soluciones que satisfacen los requisitos funcionales inmediatos pero que carecen de la capacidad de síntesis necesaria para la abstracción.

Al delegar la elección de la estrategia de implementación, el desarrollador humano cae en una "visión de túnel"; el salto desde el requerimiento al código fuente es inmediato. La tarea de validar qué fue escrito —actividad relacionada a la mecánica— es inmediatamente posterior a la recepción del requerimiento, eliminando toda percepción de valor agregado al proceso por el cual se toman decisiones de carácter esencial. Es entonces donde la unidad del sistema se fragmenta:

  • A nivel local: La solución es eficiente, técnicamente correcta a nivel sintáctico y produce una reacción a la percepción humana del tiempo sustancialmente diferente a la del proceso tradicional.

  • A nivel sistémico: Se pierde la elegancia arquitectónica. La IA no comprende la teleología del sistema ni posee la intertemporalidad intrínseca del mismo, resultando en un crecimiento entrópico del codebase.

3. Consecuencias en la arquitectura de software 

La abstracción es el mecanismo principal para gestionar la complejidad. Sin embargo, el juicio a posteriori es intrínsecamente reactivo y tiende a aceptar la vía de menor resistencia cognitiva. Si la propuesta de la IA "funciona", el incentivo para refactorizar hacia una abstracción superior desaparece.

A largo plazo, esto produce sistemas compuestos por soluciones concretas inconexas, aumentando la deuda técnica y reduciendo la mantenibilidad, dado que la "visión de conjunto" —el Big Picture— ha sido sacrificada en el altar de la velocidad de entrega.

Conclusión 

La ingeniería de software no debe confundirse con la mera producción de código; es una disciplina de diseño de sistemas. Valorar y mantener el ejercicio del juicio a priori es imperativo para evitar que la asistencia tecnológica degrade nuestra capacidad de construir sistemas robustos y coherentes, fundamentados en abstracciones interconexas que brinden unicidad y completitud. La IA debe ser un instrumento de ejecución, no el árbitro de nuestra arquitectura mental.

#SoftwareEngineering #SoftwareArchitecture #ComputerScience #GenerativeAI #CleanCode #TechLeadership #EngineeringMindset #PhilosophyOfTech #AIProgramming #TechDebt 

lunes, 17 de marzo de 2025

Los grafos no son estructuras de datos

 Enlace al artículo original

En la vida de un programador, te toca lidiar con miles de estructuras. En otras palabras, siempre estás decidiendo cómo organizar tus datos. Hay muchas opciones, desde variables simples hasta matrices complejas de estructuras definidas a medida. Uno de esos enfoques comunes son los grafos.

Pero los grafos no son estructuras de datos: son estructuras matemáticas.

Por supuesto, puedes aplicarlos como estructuras de datos; muchísimas piezas de software dependen de ellos. Incluso si no te das cuenta, estás tratando con grafos todo el tiempo. Por ejemplo, una lista enlazada es solo un caso especial de un grafo dirigido.

Pero cuando consideramos a los grafos como construcciones matemáticas en lugar de meras estructuras de datos, se convierten en una herramienta mucho más poderosa. Aquí presento dos aplicaciones más de los grafos en el desarrollo de software: como diseño de procesamiento y como representación de la base de código.

Nota: Aunque este es un post de programación general, utilizaré Go para los ejemplos de código.


Procesamiento mediante grafos

Abordemos este problema: Tenemos que procesar la temperatura mínima diaria de una ciudad (X), calculando y luego la raíz cuadrada del resultado o su logaritmo natural opuesto. Después, almacenamos el resultado en un mapa, usando el resultado como clave y su certificado como valor.

Pero debemos tener cuidado con:

  • Los números son enteros de 8 bits con signo (-128 a 127).

  • Manejo de grupos de números reales.

  • Si , calcular dará un error matemático. En tales casos, el programa usará .

  • Si desborda el valor máximo de un int8, se buscará .

  • Al buscar el logaritmo natural (), pediremos a un servicio remoto una firma de error.

¿Es un problema realista? Por supuesto que no. Pero la idea abstracta está presente. Pensemos en un enfoque tradicional:

func process(input []int8, certs map[float64]string) { for i := 0; i < len(input); i++ { var result float64 var needCert bool var cert = "no_need" x := input[i] switch { case x > 0 && x*x*x < 0: // probando escenarios de desbordamiento (overflow) x = x * x * x // queremos el mismo comportamiento que si x < 0 fallthrough case x < 0: needCert = true result = math.Log(float64(-x)) default: x = x * x * x result = math.Sqrt(float64(x)) } if needCert { cert = askCert(x) } certs[result] = cert } }

Aquí tenemos problemas: estamos procesando las entradas una por una, intentando hacer todo en el mismo lugar, procesando en lote (batch) y esperando la respuesta de askCert(x) antes de procesar otra entrada. En resumen, no estamos siendo eficientes ni aplicando buenas prácticas de código limpio (clean code).

Para limpiar este desastre, podemos tomar un enfoque diferente: grafos. Asumamos que tratamos los procesos como vértices y las comunicaciones como aristas. Cada proceso realizará una de nuestras operaciones atómicas: , , y , y la comunicación se hará a través de canales (channels).

Para implementar esto, creemos cada función asumiendo que se ejecutará indefinidamente (como nodos). Luego, les pasamos los canales de entrada y salida (aristas). El enfoque más claro para declarar aristas es definir primero una matriz de adyacencia, luego especificar los tipos de canales correspondientes y, finalmente, asignar pesos a las aristas (por ejemplo, usando tamaños de búfer en Go). Por supuesto, hacerlo así no es estrictamente necesario; en el ejemplo de la implementación completa de abajo declaro cada canal manualmente, esperando que sea más claro en sus intenciones:

Implementación de código en Playground

Pero, ¿por qué querríamos crear este lío? Por dos factores principales: control y rendimiento. Nuestro ejemplo es solo una base para empezar a pensar. Los procesos son tan flexibles como podamos imaginar. Uno de los escenarios más importantes para aplicar esto es cuando tienes muchas operaciones de E/S (I/O); puedes usar un proceso de vértice para iniciar llamadas en segundo plano a tu operación de E/S y enviar su respuesta a algún otro vértice donde sepas que se usará. Eso reduciría el tiempo de espera sin comprometer el recurso remoto, evitando una sobrecarga de llamadas (una abstracción de un sistema de inventario "justo a tiempo").

Este enfoque fue aplicado para mejorar el procesamiento de datos de diversos clientes, logrando una mejora de 16 veces en la eficiencia del tiempo combinado y la utilización de recursos.

 


El código como grafo

Otro beneficio es que ayuda en el estudio y mantenimiento del software. Puedes aplicar la teoría de grafos para entender tu base de código e incluso predecir su comportamiento antes de escribir una sola línea de código.

Estudiar el código por sí mismo es algo que, como programadores, no solemos hacer. Pero deberíamos. Este tema ha sido estudiado por Germán Cárdenas en su publicación "Evaluating the Graph-based Visualization Technique: A Controlled Experiment".

La aplicación de grafos para estudiar bases de código no es una idea nueva. Allá por el 2002, Eva Van Emden y Leon Moonen publicaron "Java Quality Assurance by Detecting Code Smells". En esa investigación, representaron "entidades" del software utilizando grafos: paquetes, clases, métodos, etc.

Tener grafos en una etapa de diseño es relativamente común, pero ¿cuánto tiempo invertimos realmente en estudiar y mantener estos grafos? En mi experiencia, casi nadie actualiza un grafo que se creó en las primeras etapas del proyecto. Además, el grafo suele servir meramente como una referencia visual más que como una herramienta de desarrollo activa.

Imagina un grafo totalmente actualizado que represente tu API REST, abarcando funciones, llamadas y modelos. Podríamos darnos cuenta rápidamente del esfuerzo que requiere un cambio. Puedes aplicar una Búsqueda en Profundidad (DFS) o una Búsqueda en Anchura (BFS) para descubrir cuántas partes de tu código se verán afectadas. Luego, toma tu Árbol de Dominadores para evaluar los riesgos de tu plan. Y, por supuesto, si el grafo se desconecta (tu conectividad es mayor a 1), notarás rápidamente que tienes código muerto con el que lidiar.

Asignemos también pesos a las aristas para representar el tiempo de ejecución esperado de las funciones. Esto nos permite estimar el tiempo de respuesta de un endpoint utilizando el algoritmo de Dijkstra. Seríamos capaces de calcular cuántos recursos necesitamos para atender el número esperado de usuarios concurrentes y qué partes de nuestro software requieren más optimizaciones.

Este tema es tan extenso como la propia teoría de grafos. Hay una gran cantidad de estudios y documentación disponible. Depende de ti, como desarrollador, adoptar este conocimiento y aplicarlo a tu trabajo diario.


Conclusión

Como desarrolladores de software, sabemos que hay demasiadas herramientas disponibles. Casi cada año se lanza al mercado una nueva tecnología o actualización con características asombrosas, y muchos esperan que nos mantengamos al día y las adoptemos lo más rápido posible para estar a la vanguardia de la industria tecnológica. Como consecuencia, nos estamos alejando cada vez más de los principios fundamentales.

Discutir periódicamente los fundamentos de las ciencias de la computación nos ayuda a redescubrir herramientas valiosas, trucos y mejores prácticas. Nos recuerda que debemos reflexionar continuamente sobre nuestras implementaciones y estudiar su comportamiento en lugar de simplemente reaplicar la misma fórmula una y otra vez.

Esta vez, se han puesto los grafos a debate. Sus ventajas son enormes. Deberíamos aprovechar todo su potencial en lugar de usarlos simplemente para estructurar datos. Recuerda:

Los grafos no son estructuras de datos.

martes, 18 de mayo de 2021

Sobre la ética

Disclaimer: Sin lugar a dudas, mi nivel sobre este tema es bastante básico. La honestidad ante todo.

¿Cuántas veces dejamos de escribir código para reflexionar sobre cuánto impacto tendrá en la comunidad en la que vivimos? Déjenme adivinar: no muchas.

Un entorno acelerado y de "hotfixes":

El mundo se ha movido más rápido que nunca en 2020, y lo sigue haciendo en este '21. Ese ticket global llamado COVID-19 nos puso a todos manos a la obra para adaptarnos, incluso a aquellos que eran los activistas más antivirtuales. Y esas tareas revelaron mucho de quiénes somos, cómo la gente de IT piensa, trabaja, imagina o, incluso, se mueve.

Somos una de las primeras industrias en adoptar el trabajo remoto como algo habitual, hace muchos años. Creamos nuestras herramientas, nuestros métodos, nuestra cultura. Y se nos señaló como los bichos raros. Pero ahora, casi todas las industrias están trabajando total o parcialmente como nosotros solemos hacerlo.

Es entonces, en este contexto caótico, cuando realmente podemos dimensionar el impacto que generamos en la vida de las personas. Y da un poco de miedo recordarnos escribiendo código sin preocuparnos por si podría dañar a alguien, o si estábamos tratando con datos personales. O incluso más allá: ¿cuántas veces notamos que algo "olía mal" en las definiciones que nos daban?

Muchísimas veces hemos tenido que lidiar con intenciones de clientes o jefes con las que podemos no estar de acuerdo, pero aun así tenemos que programar. ¿Hicimos lo suficiente para proteger la integridad de todos? ¿O hemos protegido nuestra propia integridad, manteniéndonos como buenos ciudadanos?

Lo que tenemos ahora:

Existen dos códigos de ética aceptados y adoptados:

Aun así, parece ser un tema realmente infravalorado en nuestra comunidad. Nunca he tenido la oportunidad de discutirlo en un entorno laboral, por ejemplo.

Mi intención

Con el fin de mejorar nuestra profesión, mi intención y compromiso es poner este tema sobre la mesa cada vez que pueda. Esta es mi promesa a Turing y Ritchie.

Además, quiero motivarlos a ser conscientes del impacto que están causando con su código... y a levantar una alerta cada vez que vean algo extraño tras bambalinas.

Saludos

¡Gracias por leer, y feliz (y ético) hacking!

sábado, 8 de mayo de 2021

Triangulación para autenticar APIs públicas

 

Hace algún tiempo, se me presentó un nuevo proyecto. Tenía que diseñar la solución.

Entre otras cosas de las que hablaré más adelante, uno de los desafíos consistía en comunicar un conjunto de servicios que estaban expuestos a la internet pública.

Aunque no me sentía cómodo con eso, era uno de los requisitos; una aplicación personalizada debía ser capaz de hablar con la API directamente, mientras que la mayoría de las aplicaciones hablarían con una API específica (punto de entrada o EP, por Entry Point).

Este EP tendría que hablar con otros servicios también.

Entonces, el problema era: ¿Cómo controlo que cada solicitud entre dos APIs esté realmente autorizada para pedir y leer información?

Todos los métodos de autenticación que había usado hasta ese momento seguían este esquema:

  1. Se me proporcionaba un par de valores clave para enviar en los encabezados (headers) cada vez.

  2. Tenía que solicitar un nuevo token cada vez que quería enviar un mensaje.

Pero esta vez quería hacer algo diferente y poner en práctica un consejo que me dio un colega con más experiencia: los triángulos tienden a ser seguros.

El tercero en discordia:

Para armar mi triángulo, creé una nueva API que iba a ser la "fuente de verdad" (source of truth) para los permisos. Llamémosla SoT.

Entonces, el algoritmo general era:

  1. Cada API tendrá su propia clave secreta.

  2. SoT tendrá las claves secretas de todos.

  3. Cuando llegue una solicitud desde el EP, se creará una transacción en el SoT.

  4. Cada solicitud será firmada por el emisor con su secreto y la transacción.

  5. El SoT será el único capaz de decir si una firma es correcta o no.

  6. Cada vez que una API reciba una solicitud, le preguntará al SoT si la firma de la solicitud es válida.

Implementación:

De hecho, programar esa solución fue bastante sencillo; necesité dos componentes de cliente y los controladores del SoT:

Desde el EP, solicitar crear y firmar una transacción:

idTransaccion = nuevo uuid()

firma = nuevo Hash(EP_CLAVE_SECRETA, idTransaccion)

// EntryPoint como nombre de la API solicitante para el campo del firmante

ok = sot.crear('EntryPoint', idTransaccion, firma)

En el SoT:

claveRemota = claves.obtener(input.firmante)

si input.firma == nuevo Hash(claveRemota, input.idTransaccion)

crearTransaccion()

sino

Error(403, 'No se puede crear la transacción')

Desde una API solicitada (como middleware del endpoint):

si !sot.validar(input.firma, input.idTransaccion, input.firmante)

Error(403, 'Firma inválida')

continuar()

Una vez que una solicitud era autenticada, otras llamadas internas podían autenticarse de la misma manera.

Sobre los secretos: deberían generarse aleatoriamente al momento del despliegue (deploy). Cada vez que desplegamos una API, renovamos su secreto y le informamos al SoT sobre el cambio. Se almacenan en variables de entorno.

Riesgos:

No encontré muchos motivos de preocupación. Mientras mantengamos los secretos realmente en secreto, no debería haber problemas.

Quien quiera realizar solicitudes no autorizadas tendría que conocer los secretos y la función de hash implementada.

Resumen:

Es bastante fácil implementar autenticaciones en triángulo por nuestra cuenta con cierto nivel de seguridad.

Lo que debemos cuidar es mantener las llaves en secreto y no exponer nuestras funciones de hash.

Saludos:

Gracias por leer. Cualquier sugerencia o comentario es bienvenido.


Productividad Ilusoria: ¿Optimización real o desplazamiento de carga cognitiva?

  Siguiendo con una serie de reflexiones sobre el escenario actual del desarrollo de software, comparto una de las ideas más controversiales...