Tuesday, October 08, 2024

Lean Software Development: Entregar lo más rápido posible

Uno de los principios fundamentales de Lean Software Development es entregar valor lo más rápido posible. Sin embargo, esto no significa simplemente aumentar el ritmo de trabajo, sino optimizar el flujo de valor, entregando con frecuencia, obteniendo feedback y usándolo para adaptarnos constantemente, mejorando así la eficiencia en la entrega de valor.

Vamos a explorar cómo lograrlo ajustando los procesos para movernos de manera eficiente y con confianza.

Velocidad y Dirección: No es sólo desarrollar más rápido

Ser más eficiente no se trata de construir más (ver The Building Fallacy), ni de estar siempre ocupados (eficiencia de recursos, ver The resource utilization trap). La velocidad, en términos físicos, tiene una dirección, y moverse rápido en la dirección equivocada es el mayor desperdicio que podemos tener (ver Eliminar desperdicios en el desarrollo). Para ir en la dirección correcta, necesitamos:

  • Entrega continua (Continous Deployment): para obtener feedback frecuente.
  • Humildad: para aceptar que no siempre sabemos lo que el cliente necesita ni cuál es la mejor solución técnica.
  • Adaptación: ajustando constantemente nuestra dirección técnica y de producto en función del feedback que recibimos.

Desarrollar rápido sin actuar ni decidir en base al feedback es correr en círculos. Nos agotamos mucho, pero no avanzamos nada. De hecho, es aún peor, porque acumulamos Coste Basal sin ningún beneficio (ver El coste basal del software).

¿Qué tan rápido es rápido?

Ir rápido, desde un punto de vista práctico, significa:

  •  Dar pasos muy pequeños en el desarrollo y la entrega (ver Desarrollando software posponiendo decisiones).
  • Asegurarse de que estos pasos duren entre unas pocas horas (segmentación técnica) y un día o día y medio (incrementos de producto).

Esto nos permite desplegar varias veces al día. Desplegar con esta frecuencia implica hacerlo bajo demanda, de forma segura y rápida. En un mundo ideal, estas entregas serían directamente al cliente final. Sin embargo, dependiendo del contexto, puede que haya entornos donde el despliegue a los clientes finales no sea posible, o solo se pueda hacer a ciertos usuarios (por ejemplo, en el desarrollo de sistemas embebidos o componentes de sistemas operativos). Estos entornos deberían ser la excepción (ver el modelo de Jocelyn Goldfin).

Hace unos 15 años, en Alea Soluciones, salíamos a producción entre tres y cinco veces por semana, lo cual no estaba mal, considerando que se trataba de sistemas instalados en los centros de datos de nuestros clientes. Posteriormente, en empresas como The Motion, Nextail y ClarityAI, todos entornos cloud multi-tenant, los equipos con los que he trabajado lograban desplegar varias veces al día.

En todos estos casos, alcanzamos esa velocidad de entrega utilizando Trunk-Based Development, TDD, Integración Continua y pairing/mob programming, es decir, aplicando prácticas de eXtreme Programming (XP).

Control y Velocidad: El Equilibrio Correcto

Tal como dijo el piloto canadiense de Fórmula 1, Gilles Villeneuve: "Si todo parece bajo control, es que no vas lo suficientemente rápido" ;)

Tener todo bajo control puede parecer ideal, pero en realidad, es una señal de que podrías ir más rápido. ¿Cuánto más rápido? Hasta que comiences a notar algunos fallos. Esta idea es clave cuando hablamos de desarrollar software a alta velocidad.

En mi experiencia, al trabajar con TBD, TDD y entrega continua, la confianza en el proceso crece y empiezas a acelerar, entregando cada vez más rápido. Inevitablemente, cometerás un error. Sin embargo, al trabajar en pequeños incrementos, el riesgo es bajo y corregir el fallo suele ser rápido. Tras corregir el error, es común que el equipo reduzca ligeramente la velocidad. 

Este ciclo de acelerar, aprender y ajustar es normal y, en mi opinión, es un signo de un equipo saludable que busca mejorar continuamente mientras mantiene un alto ritmo de entrega.

Necesidad de solidez en los pipelines de despliegue

En Lean, el flujo de valor comienza con una idea o experimento que queremos implementar o validar, y finaliza cuando hemos entregado el incremento, obtenido feedback y estamos listos para evaluarlo y decidir los próximos pasos.  

Aunque en la manufactura Lean se promueve la reducción de la variabilidad como una forma de estandarizar y optimizar el flujo, en Lean Product Development o Lean Software Development no todas las fases deben tener baja variabilidad. Por ejemplo, al inicio del flujo de valor, se requiere experimentación y creatividad para generar ideas y enfoques variados. Lo mismo ocurre al final del ciclo, cuando analizamos el feedback para decidir qué ajustes realizar.

Por otro lado, en la fase correspondiente al proceso “sistemático” de poner en producción, debemos asegurarnos de que haya mínima variabilidad. Esta etapa debe ser rápida, sólida y confiable, algo en lo que podamos confiar sin problemas.  

El objetivo es que los despliegues sean aburridos, una tarea rutinaria que se ejecute en piloto automático.

Para ello, los pipelines de despliegue deben cumplir con las siguientes características:

  • Ser rápidos (<10 minutos) y confiables.
  • Ofrecer un alto grado de confianza en la solución mediante buenos tests automáticos.
  • No permitir la existencia de tests inconsistentes (flaky tests).

Esto solo es posible si consideramos a los pipelines de despliegue como ciudadanos de primer nivel dentro de nuestro sistema, desarrollándolos y manteniéndolos con el mismo nivel de calidad que el resto del sistema.

Entrega continua (Continuous Deployment): El método más eficiente

Idealmente, los cambios que realizamos deberían desplegarse a producción de forma individual, uno detrás de otro. Esto nos permite obtener feedback rápidamente y entregar incrementos de valor de manera continua. Como mencionamos en este artículo y en algunos anteriores, esta forma de trabajar ofrece grandes ventajas:

  • Ajustamos continuamente el rumbo basado en el feedback.
  • Si cometemos un error, el impacto es bajo, ya que se trata de un cambio pequeño y es fácil recuperarnos al revertir a la versión anterior.
  • Además, entender cualquier error es más sencillo, porque el cambio que lo pudo haber causado está más acotado.

Sin embargo, desplegar no es gratis, tiene un coste asociado. Requiere inversión en:

  • Desarrollo de pipelines de despliegue automáticos.
  • Tiempo de espera cada vez que ejecutamos el pipeline.

Este coste se denomina "coste de transacción", y cuando es alto, limita la velocidad con la que podemos entregar.

Mantener bajos los costos de transacción

Lean Software Development recomienda mantener el coste de transacción lo más bajo posible. Para conseguirlo, es fundamental:

  • Invertir en pipelines de despliegue automáticos.
  • Usar pruebas automatizadas que nos permitan ir a producción directamente si los tests pasan.

Esto nos brinda la confianza necesaria para desplegar sin intervención manual y garantiza la calidad en cada entrega a producción.

En los equipos en los que he trabajado, nuestra opción por defecto es el Deployment continuo. Esto significa que cualquier cambio que subimos a la rama principal va directamente a producción. Para lograrlo, separamos el deployment del release mediante feature toggles, lo que nos permite liberar nuevas funcionalidades de forma controlada, sin que estén visibles inmediatamente para los usuarios finales.



Además, hemos logrado mantener los tiempos de ejecución de nuestros pipelines de despliegue entre 10 y 15 minutos, asegurando que el ciclo de entrega sea ágil y no interrumpa el flujo de trabajo. Si algo falla, solemos implementar un rollback automático cuando los smoke tests post-despliegue detectan un problema. Esto minimiza el impacto y asegura que podemos recuperar rápidamente el control.

Como destacan Jez Humble, Joanne Molesky y Barry O'Reilly en Lean Enterprise:

"The goal of continuous delivery is to make it safe and economic to work in small batches. This in turn leads to shorter lead times, higher quality, and lower costs."

Este enfoque nos permite trabajar con confianza en cambios pequeños, acelerar los tiempos de entrega, y reducir el riesgo y coste de errores.

Conclusiones

Para entregar valor rápidamente en Lean Software Development, es crucial optimizar el flujo de trabajo sin comprometer la calidad. La velocidad no implica trabajar más rápido sin control, sino ajustar continuamente en función del feedback, permitiendo una adaptación constante. Los equipos que siguen esta filosofía logran entregas continuas y seguras, minimizando riesgos y maximizando valor.

Los principios clave son:

  • Pasos pequeños y frecuentes: Entregar en incrementos pequeños reduce el riesgo y facilita la corrección rápida de errores.
  • Costes de transacción bajos: Invertir en pipelines de despliegue rápidos y fiables permite mantener la velocidad sin afectar la calidad.
  • Feedback continuo: Entregar rápidamente permite recibir retroalimentación constante, lo que facilita mejorar y adaptar el producto.
  • Confianza en la calidad: Con pruebas automatizadas y un flujo de entrega sólido, los equipos pueden estar seguros de que cada iteración libera un producto robusto.

En resumen, este enfoque no solo optimiza la entrega de valor, sino que también permite a los equipos operar de manera sostenible, con ajustes rápidos y minimizando el impacto de los errores.


Recursos relacionados

Sunday, September 29, 2024

The Building Fallacy: A Misleading Metaphor in the Age of Continuous Evolution

The "building fallacy" in software development refers to the flawed comparison between creating software and constructing a physical building. While this analogy may seem intuitive at first glance, it breaks down when we consider the nature of software and the realities of today's technology landscape. This misconception leads to inefficient processes, increased costs, and diminished capacity for innovation.


Here's why this metaphor is misleading and why it's more critical than ever to abandon it:

Why the Building Metaphor Doesn’t Hold Up

Software is Evolutionary, Buildings are Static

Buildings are designed with a fixed endpoint in mind. Once constructed, they primarily require maintenance to address wear and tear. Software, however, exists in a state of constant change. To remain relevant, it requires ongoing adaptation in response to evolving user needs, market demands, and technological advancements. Software development is more like tending a garden than erecting a structure—it requires continuous nurturing, pruning, and adjustment to thrive.

Overemphasis on Upfront Design

The building analogy often leads teams to prioritize extensive upfront planning and design, mirroring the blueprints used in construction. However, in software development, excessive upfront design can stifle innovation and lead to the dreaded "waterfall" model, where changes become costly and time-consuming. A more iterative approach allows teams to learn and adapt based on real feedback, making decisions at the last responsible moment and incorporating the latest insights.

Ignoring the Long Tail of Costs

The building fallacy promotes the misconception that most of the effort lies in the initial "construction" phase. In reality, maintaining, evolving, and supporting software systems far outweighs initial development costs.

A crucial concept that challenges the building analogy is Basal Cost—the recurring expense of maintaining features or code regardless of usage. This includes updates, security patches, and the cognitive load of supporting the code. Ignoring these costs leads to an incomplete understanding of the true financial and resource implications of software development. See more at Basal Cost of software.

Why the Building Fallacy is More Problematic Than Ever

In today's rapidly evolving tech landscape, clinging to the building fallacy is more detrimental than ever. Here's why:

The Pace of Change is Relentless

Technology, user needs, and market dynamics are shifting at an unprecedented pace. Software must evolve quickly to stay relevant. A rigid, building-centric mindset makes it hard to adapt to new challenges or opportunities.

Increased Reliance on External Systems

Modern software heavily depends on external APIs, cloud services, and open-source libraries, all of which evolve constantly. The building analogy fails to account for the complexity of managing these dependencies and ensuring seamless integration over time.

The Rise of Continuous Delivery

Continuous delivery and DevOps practices emphasize frequent releases, rapid iteration, and tight feedback loops, clashing with the notion of a fixed endpoint or grand unveiling. These practices underscore the need for flexibility, not rigidity.

Shifting to an Evolutionary Mindset

To succeed in today's environment, teams must abandon the building fallacy and embrace software as a living, evolving system. This shift involves:

Prioritizing Learning and Adaptation

Software development should be seen as a continuous process of experimentation. This means adopting agile methodologies, prioritizing validated learning over feature delivery, and delaying decisions to incorporate the latest insights.

Designing for Change

Architectures should be modular, flexible, and loosely coupled, making it easier to adapt to new requirements, integrate evolving external systems, and experiment with new technologies.

Managing Basal Costs

Teams must actively manage Basal Costs throughout the software lifecycle by:

  • Regular Maintenance: Keeping dependencies updated, applying security patches, and fixing bugs.
  • Cognitive Load Management: Simplifying the codebase and documentation to reduce the mental effort for developers.
  • Eliminating Unused Features: Removing unnecessary features to lower maintenance costs.
  • Investing in Code Quality: Refactoring, code reviews, and automated testing to improve maintainability and manage Basal Costs over time.

Lean Software Development and Extreme Programming (XP) practices are particularly effective in keeping Basal Costs low. Lean principles focus on eliminating waste, such as unnecessary code or features that add no value, which reduces maintenance burdens. XP practices, like Test-Driven Development (TDD), continuous integration, and pair programming, promote clean, maintainable code and help prevent technical debt.

By adopting these practices and embracing a mindset of continuous evolution, teams can break free from the outdated building fallacy and create software that thrives in today's dynamic technology environment.


Related content:

Sunday, September 22, 2024

Desarrollando software: posponiendo decisiones y trabajando en pasos pequeños

En esta entrega de la serie sobre Lean Software Development, después de haber explorado prácticas para posponer decisiones en el producto, hoy hablaremos sobre cómo desarrollar software dando pasos muy pequeños, aplazando decisiones, y haciendo solo lo necesario en cada momento.

Este enfoque está alineado con los principios de Lean Software Development y eXtreme Programming (XP), siendo clave en el desarrollo ágil.

Por qué trabajar en pasos pequeños

Trabajar en pequeños pasos es esencial en entornos de incertidumbre. Tanto nosotros como el cliente no siempre sabemos con exactitud qué se necesita para lograr el impacto deseado. Al avanzar en incrementos reducidos, obtenemos feedback valioso tanto del sistema —sobre su funcionamiento y comportamiento— como del cliente. Este enfoque nos permite aprender y ajustarnos constantemente, evitando decisiones prematuras que podrían limitar nuestras opciones o ser difíciles de revertir.

Es un proceso de aprendizaje continuo donde evitamos el diseño especulativo y las funcionalidades innecesarias. Al avanzar poco a poco, aceptamos que no sabemos todo desde el principio y optamos por experimentar y validar de forma constante.

Beneficios de trabajar en pasos pequeños

Trabajar en pasos pequeños con feedback continuo ofrece numerosos beneficios. Geepaw Hill, en su artículo "MMMSS: The Intrinsic Benefit of Steps", describe de forma brillante los efectos de esta práctica en los equipos. A continuación, hago un resumen, aunque recomiendo leer el artículo completo o la serie "Many More Much Smaller Steps".

Geepaw menciona ocho beneficios de trabajar en pasos de menos de un par de horas:

Beneficios en la capacidad de respuesta:

  • Interruptibilidad: Puedes manejar una interrupción o cambiar de tema sin romper el flujo de trabajo.
  • Capacidad de maniobra (Steerability): Después de cada pequeño paso, puedes reflexionar, incorporar feedback y ajustar la dirección si es necesario.
  • Reversibilidad: Si un paso no cumple con las expectativas, revertirlo implica una pequeña pérdida de tiempo.
  • Paralelismo controlado (Target Parallelism): Al avanzar en pequeños pasos consistentes, es posible trabajar en diferentes áreas del sistema o para distintos stakeholders sin dejar tareas a medias.

Beneficios humanos:

  • Alcance: Obliga a reducir la carga cognitiva, limitando las combinaciones y casos que debemos considerar.
  • Ritmo: Establece un ritmo constante en el equipo, con ciclos de recompensas rápidas (tests exitosos, commits, despliegues, etc.).
  • Seguridad: Los cambios pequeños conllevan menos riesgo que los grandes. Con tests frecuentes y despliegues diarios, el riesgo máximo es revertir el último cambio.
  • Autonomía: Permite que el equipo tome decisiones continuas, lo que requiere un esfuerzo constante por comprender y empatizar con el usuario para abordar problemas o implementar mejoras.

Trabajando en pasos pequeños y posponiendo decisiones

Desde aproximadamente 2009-2010, he intentado aplicar la práctica de trabajar en pasos muy pequeños en todos los equipos con los que colaboro. Estos pasos suelen durar pocas horas, permitiendo lanzar a producción varias veces al día y lograr cambios visibles para el cliente en uno o dos días, como máximo. Este enfoque ágil minimiza el riesgo y maximiza la capacidad de respuesta, pero requiere disciplina y la aplicación rigurosa de las prácticas de desarrollo ágil que propone eXtreme Programming (XP).

Prácticas y tácticas para trabajar en pasos pequeños

A continuación, presento algunas prácticas y estrategias que nos permiten trabajar de esta manera. A veces es difícil separarlas, ya que están estrechamente interrelacionadas y se complementan entre sí.

Desarrollo Iterativo e Incremental

La técnica más importante que usamos es también la más sencilla y, al mismo tiempo, la menos común. En lugar de partir de una solución completa y dividirla para implementarla en pasos, hacemos crecer la solución progresivamente hasta que sea suficientemente buena y podamos pasar a invertir en otro problema. Es decir, nos centramos en, teniendo la solución y el problema en mente, poner en producción (al cliente final) incrementos que estén alineados con la idea de la solución que buscamos. Usamos el feedback para asegurarnos de que vamos en la dirección correcta. Además, no tener miedo a iterar en base a este feedback nos permite trabajar en pasos pequeños y de bajo riesgo.


Por ejemplo, en este caso, partiendo de un problema inicial con una potencial solución, vamos generando los incrementos (Inc 1, Inc 2, etc.) de menos de un día. Cada incremento se entrega al usuario para obtener feedback, lo que nos ayuda a decidir el siguiente paso y si la solución ya es suficientemente buena. Así, evitamos desperdicio (zona gris) al no hacer tareas innecesarias, lo que reduce el coste basal del sistema.



Segmentación vertical (Vertical Slicing)

La segmentación vertical consiste en dividir las funcionalidades y soluciones de manera que podamos tener una aproximación incremental al desarrollo y que cada pequeño incremento aporte valor por sí mismo. Este valor puede reflejarse en mejoras para el usuario, aprendizaje para el equipo, reducción de la incertidumbre, entre otros. En lugar de dividir las historias por capas técnicas (infraestructura, backend, frontend), las dividimos por incrementos que aportan valor y, por lo general, requieren trabajo en todas las capas.

En mis equipos, aplicamos esta segmentación de manera rigurosa, procurando que ningún incremento tome más de dos días y, preferiblemente, menos de un día. Utilizamos diversas heurísticas y procesos para realizar el vertical slicing (https://www.humanizingwork.com/the-humanizing-work-guide-to-splitting-user-stories/), como el método de la hamburguesa de Gojko Adzic, que describiré más adelante.

Aunque usemos esta segmentación vertical para dividir en incrementos lo que queremos implementar, esto no implica que siempre implementemos todos los incrementos identificados. Al contrario, el objetivo es siempre hacer crecer la solución lo mínimo posible para conseguir el impacto deseado.



Abraham Vallez describe muy bien como hacer crecer progresivamente una solución en esta serie de posts 

Segmentación técnica

Como complemento a la segmentación vertical (Vertical Slicing), en mis equipos también dividimos esos incrementos que aportan valor al usuario en tareas más pequeñas que igualmente ponemos en producción. Estas tareas tienen un enfoque más técnico y suelen durar menos de dos o tres horas.

Desplegar estos incrementos técnicos nos permite obtener feedback principalmente del sistema: ¿sigue funcionando bien nuestro pipeline de CI?, ¿genera algún problema evidente el código que hemos desplegado?, ¿afecta de alguna manera al rendimiento?

Esta práctica nos obliga a mantener un coste de despliegue bajo (en tiempo y esfuerzo) y nos permite garantizar en todo momento que el flujo de trabajo sigue funcionando correctamente. Es posible porque contamos con un sistema de pruebas automatizado sólido, pipelines de CI rápidos y trabajamos con Integración Continua/Trunk-Based Development, como explicaremos posteriormente.

Poder aplicar esta segmentación técnica también es esencial para hacer cambios en paralelo, realizar modificaciones importantes en pasos pequeños y seguros, y así reducir significativamente el riesgo.

Generación de opciones

Generar opciones es fundamental para tomar decisiones bien fundamentadas. Cada decisión debe considerar múltiples alternativas; nosotros solemos intentar tener al menos tres o cuatro. Para facilitar la generación de opciones, podemos plantearnos preguntas como:

  • ¿Qué otras opciones podrías considerar si tuvieras la mitad del tiempo?
  • ¿Qué opciones requieren nuevas dependencias?
  • ¿Qué soluciones has implementado en problemas similares en el pasado?
  • ¿Cuál es el mínimo grado de sofisticación necesario para la solución?
  • ¿Quiénes podrían beneficiarse del cambio? ¿Podríamos entregarlo a cada grupo de usuarios de forma independiente?

Estas preguntas nos ayudan a generar opciones que luego el equipo puede evaluar, intentando siempre seleccionar aquellas que aporten valor rápidamente (aprendizaje, capacidad, eliminación de incertidumbre, etc.) comprometiéndonos lo mínimo posible.

Esta forma de trabajar nos permite avanzar en pequeños pasos, teniendo siempre visibilidad sobre distintas opciones que podemos tomar para continuar con el problema o redirigirlo si los pasos dados no están logrando el impacto esperado. Como ves, todo converge en trabajar con pequeños avances, aprendiendo, tomando decisiones lo más tarde posible e intentando que las soluciones sean lo más simples posibles.

Una herramienta que usamos mucho para generar opciones y realizar la segmentación vertical (vertical slicing) es el método de la hamburguesa (https://gojko.net/2012/01/23/splitting-user-stories-the-hamburger-method/) de Gojko Adzic (https://gojko.net/).

Con este método, intentamos dividir una funcionalidad o solución en los pasos necesarios para aportar valor al usuario. Esos pasos los visualizamos como “capas” de la hamburguesa, y para cada una de ellas nos forzamos a generar al menos tres o cuatro opciones. Luego seleccionamos al menos una opción de cada capa para decidir cuál será el primer incremento a implementar. Una vez implementado y entregado ese primer incremento, y con el feedback del usuario en mano, repetimos el proceso para implementar alguna de las otras opciones.

Este proceso continuo no termina cuando implementamos todas las opciones identificadas, sino cuando la funcionalidad es lo suficientemente buena o hay otra funcionalidad o problema más prioritario en el que invertir. Es decir, invertimos en lo más importante hasta que el usuario está satisfecho o hasta que surge una nueva prioridad.


Simplicidad

La simplicidad es uno de los valores fundamentales de XP y, por lo tanto, de la agilidad bien entendida. Un mantra del desarrollo ágil es “haz la cosa más simple que pueda funcionar”. Esto significa empezar con la solución más sencilla y mínima que funcione, iterando y mejorando en base al feedback.

No siempre la solución más simple es la más fácil de implementar. A veces, evitar la complejidad innecesaria requiere un esfuerzo significativo. La verdadera simplicidad es el resultado de un diseño consciente que evoluciona gradualmente.

Desarrollo en dos pasos

Kent Beck nos aconseja hacer “la cosa más simple que pueda funcionar”, pero esto a menudo se confunde con “lo primero que se me ocurra” o “lo único que sé hacer”. Una forma efectiva de asegurarnos de que estamos eligiendo la opción más simple posible es dividir cualquier cambio o incremento en dos partes:

  1. Preparación: Ajustar la base de código actual para que la nueva funcionalidad sea fácil de introducir.
  2. Implementación: Introducir el cambio real.

https://x.com/eferro/status/1810067147726508033

Esta separación evita el diseño especulativo y garantiza que solo se realicen los cambios mínimos necesarios para integrar la nueva funcionalidad, siguiendo el principio de Kent Beck: “Haz que el cambio sea fácil, y luego haz el cambio fácil”.


Principio YAGNI (You Aren't Gonna Need It / No lo vas a necesitar)

Relacionado con el punto anterior, el principio YAGNI nos recuerda que muchas ideas que se nos ocurren probablemente no serán necesarias, y que debemos enfocarnos solo en lo que necesitamos ahora. Nos ayuda a evitar el diseño especulativo y a centrarnos en lo que es realmente relevante en el momento. Incluso si identificamos algo que podríamos necesitar en el futuro cercano, YAGNI nos insta a cuestionarnos si es realmente relevante para las necesidades actuales, recordándonos que deberíamos posponerlo. Si el sistema es sencillo y fácil de evolucionar, más adelante será fácil introducir esos cambios.

Desarrollo Dirigido por Pruebas (TDD) y Outside-In TDD

El Desarrollo Dirigido por Pruebas (TDD) es una práctica que consiste en escribir primero una prueba que defina el comportamiento deseado de una funcionalidad, antes de escribir el código que lo implemente. A partir de ahí, el desarrollador escribe el código mínimo necesario para que la prueba pase, seguido de un proceso de refactorización para mejorar el diseño del código sin cambiar su comportamiento. Este ciclo se repite continuamente, lo que garantiza que cada línea de código tiene un propósito claro y definido, evitando el código innecesario o superfluo.

Outside-In TDD es una variante de TDD que comienza desde los casos de uso de negocio más amplios y se adentra en la implementación del sistema. Al partir de las necesidades del negocio y escribir únicamente el código necesario para pasar cada prueba en cada nivel (desde el nivel funcional más alto hasta las piezas individuales de código), se asegura que solo se crea el código esencial. Este enfoque previene la creación de código innecesario o la introducción de características que no son requeridas en el momento, evitando así el diseño especulativo y asegurando que se sigue siempre el principio YAGNI (You Aren't Gonna Need It).

En nuestro equipo, usamos Outside-In TDD como la forma de trabajo predeterminada para todo el código nuevo, excepto en aquellos casos donde este flujo no resulta beneficioso (spikes, algoritmos complejos, etc.). Esto implica que aproximadamente un 5-10% del código puede ser experimental para aprender, el cual es desechado posteriormente y no suele tener pruebas. Otro 10% del código corresponde a tareas donde las pruebas se escriben después (por ejemplo, integración de librerías o algoritmos complejos). El resto, la mayoría del código, se desarrolla con TDD Outside-In.

Este enfoque minimiza el desperdicio y, por defecto, sigue el principio YAGNI, ya que no se puede crear código ni diseño que no corresponda al incremento actual. Dado que el incremento actual está definido mediante una segmentación vertical radical, trabajamos en pasos pequeños, con poco desperdicio y tomando decisiones lo más tarde posible.

Como ventaja adicional, este proceso facilita la resolución rápida de errores, tanto en el código como en el diseño, ya que se verifican constantemente los avances paso a paso. Cuando se detecta un error, lo más probable es que esté en la última prueba o en el último cambio realizado, lo que permite una recuperación rápida y sin estrés.

Integración Continua, también conocida como Trunk-Based Development

Si hay una práctica técnica que obliga y ayuda a trabajar en pasos pequeños, con feedback constante, permitiendo decidir lo más tarde posible mientras aprendemos y nos adaptamos a la máxima velocidad, esa es la Integración Continua.

Primero, es importante aclarar que la Integración Continua es una práctica de XP (eXtreme Programming) que consiste en que todos los miembros del equipo integren su código en una rama principal de forma frecuente (al menos una vez al día). En otras palabras, esta práctica es equivalente a hacer Trunk-Based Development, donde solo existe una rama principal sobre la que todos los desarrolladores realizan cambios (normalmente en pareja o en equipo).

Esta práctica no tiene nada que ver con ejecutar tests automáticos en feature branches. De hecho, diría que es directamente incompatible con trabajar en ramas separadas para cada funcionalidad.

Desafortunadamente, este enfoque no es el más común en la industria, pero puedo asegurar que, junto con el TDD, es una de las prácticas que más impacto tiene en los equipos. En todos los equipos en los que he trabajado, la introducción de Integración Continua/TBD ha provocado un cambio espectacular. Nos ha obligado a trabajar en pasos muy pequeños (pero seguros), dándonos la agilidad y adaptabilidad que buscábamos.

Evidentemente, como cualquier práctica, requiere esfuerzo y el aprendizaje de una serie de tácticas para poder desplegar a producción de forma muy frecuente sin mostrar funcionalidades incompletas al usuario. Es necesario dominar estrategias que separen el deployment (decisión técnica) de la release al usuario (decisión de negocio). Las más comunes son:

  • Feature toggles: Permiten activar o desactivar funcionalidades, realizar pruebas A/B o mostrar nuevas características solo a ciertos clientes (internos, beta testers, etc.).
  • Despliegue gradual: Métodos como Canary releases o Ring deployments, que permiten realizar un despliegue progresivo del cambio.
  • Dark launches: Lanzar una funcionalidad sin que esté visible al cliente, solo para realizar pruebas de rendimiento o compatibilidad.
  • Shadow launches: Ejecutar un nuevo algoritmo o proceso en paralelo al anterior, pero sin mostrar los resultados al cliente final.

Diseño evolutivo

Esta práctica central de eXtreme Programming (XP) nos permite desarrollar software de manera incremental, refactorizando continuamente el diseño para hacerlo evolucionar conforme a las necesidades del negocio. En la práctica, consiste en crear el diseño más simple posible que cumpla con los requisitos actuales, y luego evolucionarlo en pequeños pasos a medida que aprendemos y agregamos nuevas funcionalidades.

Dentro del diseño evolutivo se pueden aplicar tácticas como:

  • Desarrollo en dos pasos.
  • Refactorización continua en el ciclo de TDD.
  • Refactorización oportunista.
  • No crear abstracciones demasiado pronto (Ver https://www.eferro.net/2015/05/aplicacion-del-principio-dry.html).
  • Cambios paralelos para mantener los tests en verde todo el tiempo mientras efectuamos cambios en varios pasos.
  • Ramificación por abstracción (Branch by abstraction) y el patrón Expandir/Contraer para facilitar cambios en paralelo.

Es importante destacar que, más allá de las tácticas que utilices para guiar el diseño en pequeños pasos, es fundamental desarrollar una sensibilidad por el diseño en el equipo. Ninguna de estas prácticas, por sí sola, enseña diseño orientado a objetos. Por lo tanto, el equipo no solo debe aprender a realizar cambios incrementales en el diseño, sino también adquirir un conocimiento profundo de los principios del diseño orientado a objetos.

Diseño evolutivo diferenciado

En general, en mis equipos intentamos siempre trabajar en pasos pequeños, enfocándonos en lo que necesitamos en ese momento y dejando que las nuevas necesidades guíen los cambios de arquitectura y diseño. Al mismo tiempo, reconocemos que la facilidad de evolución y la fricción generada ante el cambio dependen mucho del tipo de código afectado. Sabemos que no es lo mismo modificar código que implementa reglas de negocio, un API interno entre equipos o un API para el cliente final.



Cada uno de estos casos tiene más o menos fricción al cambio (es decir, distinta facilidad de evolución). Por tanto, aplicamos un diseño evolutivo diferenciado según el tipo de código.

Para el código con mayor fricción al cambio, como un API destinado al cliente final, dedicamos más tiempo a un diseño robusto que permita evolucionar sin necesidad de cambios frecuentes. En cambio, para el código interno de lógica de negocio, que solo se usa en casos específicos, aplicamos una aproximación evolutiva más flexible, permitiendo que el diseño emerja del propio proceso de desarrollo.


Otras tácticas y prácticas

Por supuesto, estas no son las únicas tácticas y prácticas a tener en cuenta, pero sí considero que son las que más nos ayudan. Existen algunos trucos y heurísticas que, aunque no los considero prácticas en sí mismas, contribuyen a la toma de decisiones y, en general, facilitan trabajar en pequeños pasos, posponiendo decisiones lo más tarde posible:

  • Priorizar librerías antes que frameworks, para no cerrar opciones y mantener mayor flexibilidad.
  • Enfocarse en hacer el código usable (y entendible) antes que en hacerlo reusable (a menos que tu negocio sea vender librerías o componentes para otros desarrolladores).
  • Usar tecnología aburrida y sólida, que esté ampliamente aceptada por la comunidad.
  • Crear wrappers ligeros sobre componentes/librerías externas para definir claramente qué parte de ese componente usamos y facilitar el testing. Puedes consultar más sobre este enfoque en https://www.eferro.net/2023/04/thin-infrastructure-wrappers.html.
  • Separar la infraestructura del código de negocio mediante Puertos y Adaptadores u otra arquitectura que permita diferenciarlos claramente.
  • Aplicar Arquitectura evolutiva para partiendo de una arquitectura mínima ir adaptandola a las necesidades de negocio, posponiendo al máximo posible las decisiones difíciles de revertir.


Conclusiones

En el desarrollo de software, la clave está en adoptar un enfoque consciente respecto a nuestras decisiones, trabajando en pasos pequeños y de bajo riesgo, y centrándonos únicamente en lo que necesitamos ahora. La simplicidad y la claridad deben ser prioridades para maximizar la eficiencia y minimizar el desperdicio.

Las prácticas de eXtreme Programming (XP), junto con los principios de Lean Software Development, nos proporcionan una guía clara para evitar el desperdicio y la sobreingeniería. Al entender que no podemos predecir el futuro con certeza, nos enfocamos en construir sistemas que sean fáciles de entender y de evolucionar, evitando la complejidad innecesaria. Trabajar de esta manera implica huir de soluciones sobredimensionadas o altamente configurables, que a menudo se convierten en obstáculos para la evolución del sistema.

Finalmente, se trata de ser humildes: asumir que no tenemos todas las respuestas y que la única forma de encontrar la solución correcta es mediante la experimentación y el aprendizaje continuo. En resumen, la simplicidad, la agilidad y la capacidad de respuesta son fundamentales para desarrollar software de manera efectiva en un entorno siempre cambiante.

Si tuviera que elegir las técnicas y prácticas que más impacto tienen en mis equipos para trabajar en pasos pequeños, seguros y posponiendo decisiones, diría que son:

  • Segmentación vertical
  • Integración Continua / Trunk-Based Development
  • TDD

Todo ello con un foco constante en la simplicidad.

Cada una de las prácticas y tácticas mencionadas en este artículo es amplia y podría explorarse en mayor profundidad. Me encantaría saber si hay interés en conocer más detalles sobre alguna de ellas, ya que sería muy enriquecedor profundizar en aquellas que resulten de mayor utilidad o curiosidad para los lectores.


Referencias y notas


Sunday, September 15, 2024

Good talks/podcasts (Sept 2024 I)



These are the best podcasts/talks I've seen/listened to recently:
  • YOW! 2019 Evolutionary Design Animated (Part1) (James Shore) [Agile, Engineering Culture, Evolutionary Design, Software Design, XP] [Duration: 00:24] (⭐⭐⭐⭐⭐) Modern software development welcomes changing requirements, even late in the process, but how can we write our software so that those changes don’t create a mess? Evolutionary design is the key. It’s a technique that emerges from Extreme Programming, the method that brought us test-driven development, merciless refactoring, and continuous integration. James Shore first encountered Extreme Programming and evolutionary design nearly 20 years ago. Initially skeptical, he’s explored its boundaries ever since. In this session, James will share what he’s learned through in-depth animations of real software projects. You’ll see how designs evolve over time and you’ll learn how and when to use evolutionary design for your own projects.
  • Eric Ries: The Science of Lean Startups (Eric Ries) [Continuous Delivery, Inspirational, Lean Startup] [Duration: 00:58] (⭐⭐⭐⭐⭐) I believe this is the best talk I have heard from Eric about the ideas of Lean Startup. It is also very enlightening to see how one of the fundamental pieces is continuous deployment and the engineering practices he uses. Essential.
  • A Conversation with Kent Beck and Eric Ries (Eric Ries, Kent Beck) [Lean Startup, XP] [Duration: 01:04] Interesting conversation between the author of Lean Startup and the author of XP. Clearly, there are synergies between the two movements, and they complement each other perfectly. With both approaches, I believe we can fully cover "Building the right thing, and building the thing right."
  • The future of software engineering (Grady Booch) [AI, Engineering Culture, Inspirational] [Duration: 01:09] (⭐⭐⭐⭐⭐) Interesting journey through the history of our profession and some predictions about the future with one of the main protagonists (Grady Booch). Very interesting, both the talk and the subsequent questions.
  • Overcomplicated Architecture: Scaling Bottleneck (Cassandra Shum) [Architecture, Architecture patterns, Engineering Culture, Evolutionary Architecture] [Duration: 00:49] Cassandra Shum discusses one of the bottlenecks of software development, an overcomplicated architecture, addressing how a company gets to an overcomplicated architecture, and how to get out of it.
  • T1 and T2 Signals (Adam Hawkins) [Devops, Lean, Operations] [Duration: 00:17] (⭐⭐⭐⭐⭐) Adam presents in this episode an interesting mental model that he calls T1 and T2 signals. It's worth understanding this model.
  • Lean Agile Brighton 2019 - Crossing the River by Feeling the Stones (Simon Wardley) [Inspirational, Product Strategy, Strategy, Technology Strategy] [Duration: 01:05] (⭐⭐⭐⭐⭐) Simon has given this talk several times, in which he presents Wardley maps and how to use them to help with strategy. I particularly like this version because it uses very good examples, including one about when to use Agile development or Lean ideas. Very instructive.
  • Platform Orchestrators: The Missing Middle of Internal Developer Platforms? (Daniel Bryant) [Platform, Platform engineering] [Duration: 00:14] Good introduction to the different approaches to platform creation for development, with an emphasis on Platform Orchestrators, which are not as well-known but can play an important role in the success of these platforms.
Reminder: All of these talks are interesting, even just listening to them.

Related:

Sunday, August 18, 2024

Good talks/podcasts (Aug 2024 I)

These are the best podcasts/talks I've seen/listened to recently:
  • YOW! 2019 Evolutionary Design Animated (Part1) (James Shore) [Agile, Engineering Culture, Evolutionary Design, Software Design, XP] [Duration: 00:24] (⭐⭐⭐⭐⭐) Modern software development welcomes changing requirements, even late in the process, but how can we write our software so that those changes don’t create a mess? Evolutionary design is the key. It’s a technique that emerges from Extreme Programming, the method that brought us test-driven development, merciless refactoring, and continuous integration. James Shore first encountered Extreme Programming and evolutionary design nearly 20 years ago. Initially skeptical, he’s explored its boundaries ever since. In this session, James will share what he’s learned through in-depth animations of real software projects. You’ll see how designs evolve over time and you’ll learn how and when to use evolutionary design for your own projects.
  • Be Quick But Don't Hurry (Joshua Kerievsky) [Agile, Culture, Inspirational] [Duration: 00:02] (⭐⭐⭐⭐⭐) Inspirational. These two minutes summarize very well why it is important to be fast but maintain control in order to be truly agile.
  • YOW! 2019 Evolutionary Design Animated (Part2) (James Shore) [Agile, Engineering Culture, Evolutionary Design, Software Design, XP] [Duration: 00:24] (⭐⭐⭐⭐⭐) The second part of this great talk. Part1 https://www.youtube.com/watch?v=LtBRvsez8DI
  • The best programmer I know (Dan North) [Agile, Engineering Career, Engineering Culture, Inspirational, Nature of Software Development] [Duration: 00:56] (⭐⭐⭐⭐⭐) Very good talk about the nature of software development and the approach to the profession. Dan talks about software as a medium, continuous learning, teamwork, etc. Highly recommended.
  • The Joy of Building Large Scale Systems (Suhail Patel) [Architecture, Engineering Culture, Microservices, Scalability] [Duration: 00:53] Suhail Patel discusses the art and practice of building systems from core principles with a focus on how this can be done in practice within teams and organisations. Very interesting talk with many details about system implementation, taking into account the changes that have occurred in the hardware.
  • Alan Kay at OOPSLA 1997 - The computer revolution hasnt happened yet (Alan Kay) [Engineering Culture, Evolutionary Design, Inspirational, Nature of Software Development, OOP, Software Design] [Duration: 01:04] (⭐⭐⭐⭐⭐) Classic presentation by Alan Kay talks about the nature of software, the design that systems should have, scalability, and how, to some extent, we could compare it to how biological systems work. Many of the ideas behind Smalltalk can be identified in the talk.. Inspirational.
  • Concurrency Oriented Programming in a Modern World (Francesco Cesarini, Robert Virding) [Architecture patterns, Microservices, Scalability, erlang] [Duration: 00:52] This talk delves into how the Erlang concurrency model, initially developed for telecom systems in the 90s, is now vital for modern cloud-based microservices, mobile apps, and machine learning. Presenters Robert and Francesco emphasize how functional languages and fault-tolerant computing principles are essential for distributed multi-core architectures across cloud, edge, and IoT networks.
  • Continuous Integration: That’s Not What They Meant (Clare Sudbery) [CI, Technical Practices, Trunk Based Development, XP] [Duration: 00:56] (⭐⭐⭐⭐⭐) Very good talk about the benefits of using Trunk Based Development (or in other words, the practice of CI as it was originally created).
  • Generative AI in a Nutshell - how to survive and thrive in the age of AI (Henrik Kniberg) [AI, Generative AI] [Duration: 00:17] Simple explanation of what AI is, Generative AI, and how it works at a high level. It's not a detailed explanation, but it's very useful if you want to explain it to someone without a technological background.
Reminder: All of these talks are interesting, even just listening to them.

Related:

Tuesday, July 16, 2024

El coste basal del software

Traduccion del articulo original Basal Cost of Software
Traduccion realizada por https://x.com/simonvlc y publicada originalmente en su genial newsletter Estrategia de Producto: El Coste Basal del Software
---

En mi opinión, el coste de desarrollo de cada funcionalidad en un producto puede dividirse de la siguiente manera:

  1. El coste directo de desarrollo o coste inicial.
  2. Un coste semanal o mensual únicamente asociado con su existencia en el sistema. Haciendo una comparación con la Tasa Metabólica Basal del cuerpo humano, podríamos llamar a este segundo coste la Tasa Metabólica Basal o Coste Basal.

El Coste Basal se compone de dos partes diferenciadas:

  • El impacto directo en la capacidad del equipo debido a la complejidad añadida (nuevas dependencias, más código para entender, más posibilidades de bugs ocultos, etc.).
  • El impacto en el coste de desarrollo o evolución de otras funcionalidades debido a posibles incompatibilidades, acoplamiento, etc.

El coste inicial

Es el coste incurrido por el equipo durante el desarrollo inicial de la funcionalidad. Incluye el coste desde que el equipo comienza a trabajar en la funcionalidad hasta que el cliente la tiene disponible y comienza a usarla. Por supuesto, este proceso debería consistir en múltiples despliegues y lanzamientos parciales para obtener feedback y hacer los ajustes necesarios...

El Coste Basal

Tras el coste inicial de desarrollo, afrontamos también un coste continuo que reduce la capacidad del equipo para el desarrollo de nuevas funcionalidades (innovación).

Este coste, que continúa con el tiempo y solo termina con la eliminación de la funcionalidad o el fin de vida del producto, es el coste que paga el equipo por la existencia de ese código en el producto.

Es importante tener en cuenta que este no se refiere al coste de hacer modificaciones o corregir errores; se refiere al coste de simplemente tener el código allí...

¿Por qué se da este coste? ¿Por qué una funcionalidad que no evoluciona supone un coste? 

  • El equipo tiene que conocer ese código (dónde está, qué dependencias tiene, qué interactúa con él, cómo está diseñado...).
  • El conocimiento y las técnicas del equipo están en constante evolución. Cuando mejora en cualquiera de estos aspectos, deberá actualizar el código.
  • Cuando el equipo diseña una nueva funcionalidad, el código debe ser diseñado de manera que sea compatible con todas las anteriores y no genere problemas o regresiones. Y, por supuesto, este coste es proporcional a todas las que ya teníamos en el sistema.
  • Algo similar ocurre cuando un nuevo miembro se une al equipo. Este nuevo integrante debe aprender sobre todas las funcionalidades, por lo que el coste es proporcional a su número..

Y lo peor de todo, este sobrecoste es continuo hasta la "muerte" de la funcionalidad. Por ejemplo, hasta el fin de la vida del producto, hasta que ningún usuario la use, o hasta el fin del mundo (lo que ocurra primero).

Evolución del coste de una funcionalidad

Como hemos visto, el Coste Basal de una funcionalidad es más o menos constante durante su vida. Pero cada lenguaje, dependencia o tecnología que hayamos usado en su desarrollo, puede alcanzar un punto en el que ya no es utilizable por cualquier razón (dependencias obsoletas, problemas de seguridad, evolución normal que deprecia la versión del lenguaje que usamos, etc.). Desde este momento, el coste puede dispararse porque estamos obligados a actualizarlo, incluso aunque no queramos evolucionar la funcionalidad.

En resumen, el coste real puede ser representado por el siguiente gráfico:

El problema

Un error común es negar el Coste Basal y considerar que si no se hacen cambios, el coste de mantenimiento de una funcionalidad es cero. Supongo que esto proviene de utilizar metáforas de otras profesiones como la construcción, que no son aplicables al desarrollo de software.


La capacidad no es infinita

A pesar de que la capacidad de un equipo cambia (positiva o negativamente) con el paso del tiempo debido a distintos factores (conocimiento del negocio, técnicas, estrés…), esta es finita.

El Coste Basal acumulado de todas las funcionalidades de las que el equipo es responsable reduce la capacidad disponible para desarrollar nuevas funcionalidades (innovar).

Con el tiempo, podemos observar como la capacidad del equipo para innovar decae rápidamente:

Hasta el punto de que cuando la capacidad se agota, el equipo se encuentra en una situación en la que le es imposible innovar porque pasa todo su tiempo “manteniendo” las funcionalidades de las que es responsable.


O bien reduce su velocidad al desarrollar nuevas funcionalidades, acumulando coste basal, pero a menor ritmo.

Conclusiones

Las secciones anteriores destacan varios principios que debemos considerar para mejorar la eficiencia de nuestros equipos de producto.

  • Debemos minimizar el Coste Basal tanto como sea posible, logrando el impacto deseado con la menor cantidad de código posible.
    • Cuando sea posible, incluso sin añadirlo.
    • Iterando la funcionalidad, partiendo de una solución mínima, para adaptarla lo más posible a las necesidades del usuario.
    • Haciendo el software/diseño lo más simple posible. Debe ser fácil de evolucionar sin sobreingeniería (YAGNI).
  • Debemos eliminar cualquier funcionalidad del producto que no tenga el impacto deseado, eliminando así su Coste Basal.
  • Debemos monitorizar continuamente el código del equipo para detectar la obsolescencia de dependencias, lenguajes y tecnologías para evitar que el Coste Basal se dispare.

Veamos un ejemplo teórico sencillo para ayudar a transmitir el mensaje. Supongamos que un equipo acumula un 1% de Coste Basal por semana, lo que significa que, por cada 100 unidades de producto (código, características, mejoras), añade 1 unidad de Coste Basal. A medida que este se acumula, en 52 semanas (aproximadamente 1 año), el equipo solo podrá dedicar el 60% de su capacidad a nuevas funcionalidades/mejoras (innovación). En dos años, será apenas el 35%. Por supuesto, este ejemplo es una simplificación, pero destaca que negar este coste no es una opción viable.


Recordad: La Simplicidad — el arte de maximizar el trabajo no hecho — es esencial.

Relacionado y referencias

Otros artículos interesantes


Monday, July 01, 2024

Decidir lo más tarde Posible: Limites de Producto

Como mencionamos en el artículo anterior de la serie sobre Lean Software Development, continuaremos explorando técnicas que nos permiten tomar decisiones lo más tarde posible.

Empezamos por definir sistemáticamente Limites de Producto.

Al desarrollar un incremento de la solución que estamos implementando, es fundamental establecer límites concretos en todos los parámetros que puedan introducir complejidad. Esto nos permite centrarnos en lo que aporta valor ahora y posponer soluciones más sofisticadas, evitando el coste y la complejidad añadida. Con el tiempo, esos límites irán cambiando y nos obligarán a modificar la solución, pero con esta aproximación podremos decidir cada solución lo más tarde posible y evitar hasta ese momento el coste de desarrollo y evolución de esa solución más compleja.


Es crucial que al definir un límite, este se incluya en el código o solución para que, si se supera, la aplicación se comporte de manera controlada, avisando al equipo y, posiblemente, al usuario.


Ejemplos de límites que he usado:

  • Número de clientes/usuarios totales.
  • Número de clientes/usuarios concurrentes.
  • Tamaño máximo de ficheros que se pueden subir al sistema.
  • Cuotas por usuario (almacenamiento, requests, número de entidades, etc.).
  • Valores numéricos por cualquier concepto de negocio (del problema).
  • Tiempos de respuesta a las distintas peticiones.
  • Resoluciones/Dispositivos para la IU

Si no definimos claramente estos límites de manera numérica, estamos abriendo la puerta al diseño especulativo para resolver situaciones que aún no enfrentamos.


Ejemplos de uso de límites para "decidir lo más tarde posible"

Conocer y definir el número total de clientes en varias ocasiones me ha permitido ofrecer soluciones muy sencillas para la persistencia, que nos han sido útiles durante meses antes de necesitar cambios. Por ejemplo, si el número de usuarios es pequeño, usar persistencia en fichero y cargar la información a memoria es factible. También nos permite usar soluciones tipo SQLite y posponer la decisión de meter un motor de BD independiente.



Al limitar el tamaño de las solicitudes (en cuanto a volumen) y definir los escenarios de conexión, podemos ofrecer soluciones robustas y simples, procesando las solicitudes de forma síncrona. Esto permite posponer la necesidad de tener una solución asíncrona. Como ejemplo, en una ocasión teníamos que aceptar que el usuario subiese ficheros al sistema; la primera implementación solo permitía ficheros muy pequeños, esto nos permitió hacer una implementación sencilla y tener feedback muy rápido (menos de 2 días). Pasadas unas semanas, una vez que vimos que tenía sentido la funcionalidad, mejoramos la implementación para soportar ficheros más grandes.


En varias ocasiones hemos enfrentado situaciones donde cada usuario acumulaba almacenamiento (archivos/objetos). En estos casos, el definir un límite de producto para el total de almacenamiento para todos los usuarios, otro límite para cada usuario y otro límite para definir cuándo teníamos que empezar a preocuparnos por este problema, nos ayudó a posponer la implementación de cualquier medida de control y gestión de este almacenamiento hasta que se llegase a cualquiera de los límites definidos.



Para ilustrar el uso sistemático de estos límites con un ejemplo concreto, en Alea Soluciones lanzamos un nuevo producto de gestión/control de una red de fibra y los routers de los clientes finales en menos de 3 meses. Sabíamos que nuestros clientes en ese momento no tenían más de 1000-2000 usuarios. Sabíamos que los operadores del sistema de gestión no eran más de 2-3 personas concurrentes. Sabíamos que para tener más usuarios nuestros clientes tenían en muchos casos que desplegar fibra hasta la casa o al menos ir a la casa del usuario a instalar el router; esto implicaba que no era posible que creciesen a más de 2-5 usuarios por semana. Con este contexto, la primera versión del sistema inicial de gestión era un servidor web que procesaba todo de forma síncrona y con la información de usuarios en memoria y que simplemente persistía los cambios a un fichero cuando se necesitaba. Esto permitió que dedicásemos mucho tiempo adicional al resto de componentes del sistema (integración con cabeceras de fibra, sistema de configuración remota de los routers, sistema de monitorización de los routers, etc.). Evidentemente, el sistema fue evolucionando y fuimos mejorando ese sistema, pero siempre esperando al último momento responsable para cada decisión de introducir nueva tecnología.


Otro ejemplo sencillo es en ClarityAI, donde para crear un chat ops en Slack que ofreciera algunas capacidades internas de la plataforma, se definieron ciertos límites, tanto en tiempos de respuesta máximos como en volumen de información tratada. Al definir un tiempo de respuesta máximo alto (2s) pero menor del soportado por slack para la respuesta sincrona a comandos de slack (3s), pudimos posponer bastante tiempo la implementación de una solución asincrona. Esa aplicación trabaja con información de inventario de componentes técnicos (repositorios de código, repositorios docker, etc) y de equipos y personas. Vimos que nos era fácil definir unos maximos para cada uno de los elementos y vimos que en todos los casos el máximo estaba por debajo de 1000 elementos. Estos limites nos permitieron evitar bastante complejidad, simplemente apoyandonos en un backend NoCode (Airtable) como BD, que además nos proporcionaba un frontal de administración básico. Sabemos perfectamente que cuando superemos esos limites, tendremos que pensar en una solución más sofisticada y escalable, pero posponer esa decisión nos ha permitido desarrollar muy rápido en esa aplicación durante más de dos años y medio.


Recursos Relacionados:

Sunday, June 30, 2024

Lean Software Development: Decidir lo Más Tarde Posible

Lean Software Development parte de la premisa de que el desarrollo de software y productos digitales es un ejercicio constante de aprendizaje (ver amplificar el aprendizaje). Con esta premisa, es claro que cuanta más información tengamos antes de tomar una decisión, mayor será la calidad de la misma. Por tanto, decidir lo más tarde posible nos permite ser más efectivos. Al mismo tiempo existe un coste (o riesgo) asociado a no tomar una decisión que crece con el tiempo, por lo que debemos buscar el “último momento responsable”, que es el punto óptimo para tomar la decisión con la mayor información posible, sin que el coste de no tomarla supere el beneficio potencial de obtener más información y seguir postergándola.

Ventajas posponer decisiones y mantener opciones abiertas

Postergar decisiones es una estrategia fundamental en el desarrollo de software lean. Aunque no siempre es fácil y requiere práctica, esta táctica permite la creación de productos sostenibles y fáciles de evolucionar. Una de las principales ventajas de tomar decisiones más tarde es que nos brinda una mejor comprensión tanto del negocio como de la tecnología, lo que a su vez facilita la toma de decisiones más informadas y acertadas.

Además, retrasar decisiones conduce a soluciones más simples y pequeñas, lo cual reduce el esfuerzo necesario para implementarlas y evita el sobrecoste basal. Manteniendo nuestras opciones abiertas, nos enfocamos únicamente en lo que aporta valor real en el presente, evitando la sobre-ingeniería y el trabajo innecesario. Esta flexibilidad nos permite reaccionar de manera rápida y efectiva a cualquier cambio sin temor, lo que es crucial en un entorno tan dinámico como el desarrollo de software.

Algunas de las ventajas específicas de posponer decisiones incluyen:

  • Menos trabajo y desperdicio: Implementar solo lo necesario en el momento reduce el trabajo total y el desperdicio.
  • Menor esfuerzo en rehacer trabajo: Si es necesario cambiar algo, hay menos esfuerzo requerido ya que se ha evitado sobre-ingeniería.
  • Mayor flexibilidad y adaptabilidad: Mantener opciones abiertas nos permite adaptarnos rápidamente a nuevos requerimientos o cambios en el entorno.


Una arquitectura bien diseñada permite posponer decisiones importantes sin comprometer la calidad o la flexibilidad del producto. Esto no solo nos da la capacidad de tomar decisiones más informadas, sino que también facilita la creación de una buena arquitectura que, a su vez, permite posponer otras decisiones críticas en el futuro. En resumen, esta estrategia nos permite movernos con menos carga y mayor agilidad, mejorando nuestra capacidad para entregar valor continuamente.

Decisiones

Mis equipos son equipos empoderados (extremo a extremo) que tienen completa decision sobre como solucionar las problemas y en muchos casos incluso en decidir qué problemas solucionar. En un equipo así, se toman numerosas decisiones todos los días y de todo tipo. Decisiones sobre el problema, sobre potenciales soluciones, sobre implementaciones de esas soluciones, sobre la priorizacion relativa de dónde invertir (en solventar algo, en despejar incertidumbre sobre una decisión técnica o de producto, en decidir siguientes incrementos, etc).

Por ejemplo, al comenzar un nuevo producto o funcionalidad, el nivel de incertidumbre suele ser muy alto. En estos casos, priorizamos reducir esa incertidumbre, dividiendo decisiones y suposiciones críticas en partes más pequeñas, e invirtiendo en incrementos de producto para obtener más feedback y así reducir la incertidumbre.


A medida que recibimos retroalimentación, aprendemos más y ajustamos nuestras suposiciones/hipótesis en consecuencia. Este proceso iterativo nos ayuda a pasar de una gran incertidumbre a una mayor claridad y confianza.

Al validar continuamente los supuestos, reducimos los riesgos y tomamos decisiones más informadas. 

Todas estas decisiones se producen de forma continua y a todos los niveles. Es parte de la naturaleza de nuestra profesión.

Si tuviesemos que clasificar el tipo de decisiones a tomar, lo podriamos hacer de la siguiente forma:

  • Decisiones sobre el problema y oportunidad a explorar
    • Qué: Donde debemos invertir y por qué
    • Cómo: Qué solución o estrategia de implementación creemos que es adecuada para el caso seleccionado.
  • Decisiones sobre tecnologia e implementacion
    • A nivel de arquitectura y tecnologias usadas
    • A nivel de diseño de la solución a diferentes niveles

Es importante tener en cuenta que las decisiones nunca son independientes; siempre deben considerar el contexto y el conocimiento del equipo, así como el sistema o producto existente. Es como si viéramos los problemas y oportunidades como la diferencia en conocimiento o implementación, de lo que conocemos o tenemos y lo que queremos conseguir.

Es decir, que siempre se trata de hacer pequeños incrementos en la dirección y con el objetivo que necesitemos. A veces se trata de aportar valor al usuario, otras veces de reducir la incertidumbre de una decisión que debemos tomar. Siempre, es un incremento en el software (producto/sistema) o en el conocimiento que tiene el equipo.

Dividir decisiones para abordarlas individualmente

Una de las formas principales de incrementar nuestras posibilidades de posponer decisiones es dividir grandes decisiones en pequeñas decisiones y separar muy bien aquellas de facil reversibilidad de las que sean irreversibles (o muy dificiles de revertir).

Por lo tanto, trabajar decidiendo lo más tarde posible implica hacerlo en pasos o incrementos muy pequeños, lo cual está totalmente alineado con la mentalidad Lean (trabajo en lotes pequeños).

Este proceso de dividir es una práctica continua que permite al equipo afrontar desde donde va a invertir en el siguiente cuatrimestre hasta decidir qué es lo que va a hacer en las siguientes horas. Debemos verlo como una forma de pensar sistemática que se aplica de manera recurrente a todos los niveles.

Conclusiones

Decidir lo más tarde posible es una estrategia clave en el Lean Software Development que maximiza la calidad de las decisiones al aprovechar la mayor cantidad de información disponible. Esta práctica contribuye a la creación de productos más sostenibles, flexibles y adaptables.

Las principales ventajas son:

  • Reducción del desperdicio: Enfocarse solo en lo necesario minimiza el trabajo redundante y evita la sobre-ingeniería.
  • Mayor flexibilidad: Mantener opciones abiertas permite adaptarse rápidamente a cambios en el entorno o requisitos.
  • Decisiones más informadas: Postergar decisiones hasta el último momento responsable resulta en decisiones más acertadas y efectivas.
  • Incremento en la adaptabilidad: Facilita la implementación de soluciones simples y pequeñas, reduciendo el esfuerzo para realizar cambios y mejoras.

Es fundamental que los equipos empoderados adopten esta mentalidad y se acostumbren a dividir las decisiones en pasos manejables. Al hacerlo, pueden reducir la incertidumbre de manera incremental, validando continuamente sus supuestos y ajustando estrategias según el feedback recibido. Este enfoque iterativo disminuye riesgos y refuerza la capacidad del equipo para entregar valor continuo.

En los siguientes artículos, exploraremos estrategias y ejemplos concretos sobre cómo posponer decisiones de manera efectiva, proporcionando herramientas prácticas para implementar esta filosofía en tus proyectos de desarrollo de software.


Recursos relacionados


Sunday, June 23, 2024

Good talks/podcasts (June 2024 I)

These are the best podcasts/talks I've seen/listened to recently:
  • (Spanish) Impulsando el crecimiento organizacional a través de la Developer Experience (Angélica Lozano, José Rodríguez Huerta) [Devex, Engineering Culture] [Duration: 01:07] Interesante conversación sobre el funcionamiento y organización de MLean con Angélica CTO y fundadora. Interesantes ideas sobre como conectar la experiencia de desarrollo con las necesidades de la compañia, la organizacion de equipos, las particularidades del negocio, como balancean las distintas inversiones en ingenieria. 
  • Software Engineering F&*K Up Behind The Passport E-gate Failure" (Dave Farley) [Architecture, Resilience, Software Design] [Duration: 00:17] In this episode, Dave Farley talks about the issue, how poor software engineering led to this, how distributed systems come into it all, how to avoid something like this happening again and at the end of the video asks for answers on some concerning issues around the whole story.
  • Perspectives on effective product development culture (Jason Yip) [Culture, Engineering Culture, Product] [Duration: 00:39] Good description of what an efficient product development culture should be like. Jason talks about the pillars of that culture, the guiding principles, and even the main practices, including technical, product, and organizational practices.
  • Lean Code (Kevin Kelly) [Agile, Lean Software Development] [Duration: 01:05] (⭐⭐⭐⭐⭐) Interesting approach to lean software development focused on how to tackle agile development with this lean mindset. It contains a few interesting points.
  • Designing for Habitability (Sam Newman) [Platform, Platform engineering] [Duration: 00:27] Tips and tricks for creating platforms that truly accelerate the pace of innovation in companies.
  • Fighting User Requirements That CONSTANTLY Change (Dave Farley) [Nature of Software Development, Product] [Duration: 00:13] (⭐⭐⭐⭐⭐) Great description of the true nature of software development as an adaptive learning process of customer needs. A must.
  • Continuous Delivery vs. Gitflow & CD At Scale | Bryan Finster In The Engineering Room Ep. 11 (Bryan Finster, Dave Farley) [Continuous Delivery, Engineering Culture, Engineering Scalability] [Duration: 01:09] Interesting conversation about how to introduce CD as part of the engineering strategy in large companies and how having CD as part of the vision (not as a goal in itself) can generate a healthy engineering culture that continuously improves.
Reminder: All of these talks are interesting, even just listening to them.

Related: