Cómo diseñamos software escalable en Fintoc
En Fintoc nos gusta escribir documentos de diseño antes de ponernos a programar. En esta etapa, un grupo de ingenieros revisa la solución que se está diseñando para incluir mejoras y atajar problemas antes de tiempo.
Pero, a pesar de que todos hemos visto u ocupado código bien diseñado, muchas veces nos cuesta poner en palabras por qué una solución es mejor o peor que otra. Por eso es clave contar con herramientas “objetivas” al momento de diseñar o revisar soluciones.
¿Qué hace que un Diseño de Software sea “bueno”?
Para responder a esta pregunta, primero hay que tener claro cuál es el objetivo del Diseño de Software. Para esto me gusta mucho la visión de “Uncle Bob” Martin en su libro Clean Architecture (para Martin no hay diferencia entre arquitectura y diseño de software).
The goal of software architecture is to minimize the human resources required to build and mantain the required system.
En otras palabras, la medida de la calidad del diseño de un software es qué tan costoso o difícil es mantener y seguir desarrollando el sistema en el tiempo. A medida que la deuda técnica se acumula y los sistemas no están bien diseñados, lo normal es que este costo aumente. Y si este no se controla, se puede llegar al punto (a quién no le ha pasado) donde es más económico construir un servicio nuevo desde cero que intentar cambiar el que ya está andando.
Entonces, ¿cómo construimos software flexible? Una de las herramientas que más nos ha ayudado últimamente es el principio de Inversión de dependencias. Pero antes de eso, necesitamos revisar algunos conceptos.
Dependencia de componentes
El principal factor que afecta la flexibilidad de un sistema es la dependencia entre sus componentes. Supongamos que tenemos dos componentes A
y B
, donde A
ocupa a B
para algún motivo.
En el momento en que B
comienza a ser usado por A
, este se rigidiza y pierde flexibilidad. Esto porque B
ya no puede cambiar libremente su implementación sin arriesgar romper al componente que depende de él. Si queremos modificar B
, al menos debemos asegurarnos de que el cambio es aceptable para A
.
Teniendo esto en mente, podemos revisar dos situaciones opuestas representadas por los componentes X
e Y
.
En la imagen anterior, X
es un componente “rígido”. Dado que X
tiene tres componentes que dependen de él, este no puede cambiar fácilmente su implementación. Mientras más componentes dependen de otro, más difícil se vuelve cambiarlo. Por otro lado, X
no depende de nadie y no hay razones externas a X
para que este cambie. Esto hace que X
sea más “estable”.
Y, en cambio, es un componente flexible. Dado que ningún componente depende de él, Y
puede cambiar libremente sin riesgo de romper a nadie. Sin embargo, dado que Y depende de muchos componentes, esto lo vuelve inestable. Cualquier cambio en A
, B
o C
podría obligarlo a adaptarse.
Ahora veamos un ejemplo real de Fintoc para entender mejor cómo afectan y cómo podemos utilizar la Inversión de Dependencias para manejar estas situaciones.
Payouts - El servicio de dispersión de Fintoc
Payouts es un Engine que construimos en Fintoc para hacer dispersiones de dinero. Originalmente nació para automatizar el pago de las recaudaciones de Débito Directo, pero luego siguió evolucionando y hoy es una pieza central de nuestra operación.
💡 Una dispersión (o Payout) es traspasar los fondos de un pago/cargo desde una cuenta de Fintoc a la cuenta de uno de nuestros clientes.
Primera versión: Payouts para solo un producto
La primera versión del proceso de creación de Payouts consistía en lo siguiente:
- Una vez al día se ejecuta un cron job (
CreatePayoutsJob
) - El job consulta todos los cobros (charges) exitosos del día anterior y crea un Payout para cada uno. Cada Payout representa el estado de la dispersión del charge correspondiente.
- Por cada Payout generado se crea una o más transferencias (debido a los límites de las transferencias bancarias, a veces se necesita más de una para poder ejecutar un Payout).
En un principio, esta versión funcionó bien. Era sencilla y nos permitió afinar el flujo operacional detrás de la aplicación. Los problemas aparecieron cuando quisimos extender su uso 😅.
Segunda versión: Idea naive para extender a más productos
Rápidamente quisimos extender el uso de Payouts para Iniciación de Pagos y dispersar el dinero recaudado a través de Payment Intents.
La primera versión que dibujamos para extender el uso del engine se veía algo así.
Este diseño solo agregaba más llamados desde el Engine de Payouts a los componentes que se debían dispersar. Aquí comenzaron a notarse los problemas:
- Payouts dependería de los modelos de los 2 engines y los que queramos agregar en un futuro. Esto para hacer cosas como:
- Obtener los recursos que hay que dispersar (filtrando por fecha)
- Obtener el monto de dinero de cada uno de los recursos
- Asociar los recursos a sus Payouts y Transferencias respectivamente.
- Payouts queda como único responsable de calendarizar las dispersiones de todos los productos. ¿Esto no debería ser responsabilidad de cada Engine?
- Para crear Payouts el engine se llenaría de lógica de cada producto:
- ¿Qué pasaría si algunos productos necesitan descontar la comisión de Fintoc antes de dispersar?
- ¿Qué pasa si el manejo de fechas por producto cambia? Por ejemplo, algunos productos podrían operarse con días bancarios y otros con días normales.
En resumen, este diseño implica que Payouts debe conocer detalles de la implementación de componentes dispersos por todo el sistema. Así, cualquier cambio posterior en estos productos se volvería más costoso dado que podría romper el flujo de dispersión.
Principio de Inversión de Dependencias
La solución a este problema está en el principio de inversión de dependencias. De nuevo, usando la definición de Clean Architecture:
The Dependency Inversion Principle tells us that the most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions.
Depender de interfaces abstractas entrega flexibilidad a los componentes, porque permite invertir la forma en que dos componentes se relacionan. Así, dos componentes pueden funcionar juntos sin que ninguno haga referencia directa al otro. Si volvemos al ejemplo del componente Y, podríamos hacer el siguiente cambio.
En este diagrama, Y
ya no depende de A
, B
ni C
directamente (clases concretas), si no que de una interfaz (abstracción). En cambio, son A
, B
y C
quienes tienen una dependencia hacia Y
. De esta forma, permitimos que los objetos concretos puedan cambiar libremente, siempre y cuando respeten el contrato definido por la interfaz.
Siguiendo este principio podemos corregir el problema del Engine de Payouts.
Versión final: Interfaz Disbursable y Payouts Service
Hicimos dos cambios:
- Creamos un
PayoutsService
- Expone una forma única de crear Payouts
- Cada engine puede llamarlo cuando quiera crear uno o más Payouts
- Creamos una interfaz
Disbursable
- Es un contrato que otros componentes deben cumplir para poder ocupar Payouts
- Cada cliente es responsable de traducir sus recursos y sus reglas de negocio en objetos “dispersables” por Payouts
Ojo que no hay ninguna dependencia desde Payouts Engine hacia otros componentes del sistema. Esto quiere decir que Payouts puede cambiar libremente su implementación sin afectar al resto del sistema. Por otro lado, Payouts no tiene ningún conocimiento de los objetos que tiene que pagar.
Con esto podemos seguir sumándole Engines al sistema, si quisiéramos automatizar la devolución de dinero de nuestros clientes a sus usuarios o para resolver descuadres operacionales.
Esto es muy útil en caso que queramos implementar más de un sistema de dispersión. Por ejemplo, podríamos tener mecanismos de dispersión para distintos bancos, o distintas formas de dispersar según los montos involucrados, o formas más o menos manuales (API v/s a través de la página de un banco).
¿Cómo seguimos escalando?
No es posible desarrollar sistemas que sean completamente flexibles. A medida que estos crecen es inevitable que algunas de sus partes se vayan rigidizando. El problema es que malos diseños de software terminan por hacer que cualquier cambio sea imposible.
A medida que se ha ido complejizando el backend de Fintoc con más componentes y dependencias entre ellos, los MVPs se van rompiendo. Esto nos exige implementar soluciones más maduras, definir buenas interfaces y fronteras claras que nos permiten escalar y seguir desarrollando con seguridad y rapidez.
El principio de Inversión de dependencias ha sido clave para lograr esto.
Si te gustó lo que leíste y te gustan los problemas difíciles, siempre estamos contratando.
Puedes ver las vacantes y postular acá.