Comunicación con pasarelas de pago y Domain-Driven Design

Alexis Martínez Chacón
Genially Tech
Published in
12 min readSep 12, 2023

--

Genially ha experimentado una gran evolución en cuanto a cómo los usuarios pueden trabajar con nuestro producto. Las necesidades de los usuarios han cambiado a lo largo del tiempo y, por ello, nos hemos tenido que adaptar rápidamente a estos nuevos requisitos. Lejos de ser un problema, es otro reto más al que nos enfrentamos. ¡Y es algo que nos encanta!

Concretamente, hemos sido testigos de cómo el modelo tradicional donde un usuario trabaja en un entorno individual y puntualmente colabora con otros usuarios se quedaba obsoleto. Es por ello por lo que nos pusimos manos a la obra para ir hacia un nuevo enfoque donde la colaboración y el uso grupal es el eje fundamental.

Este cambio de paradigma ha tenido un gran impacto a todos los niveles en Genially ya que a fin de cuentas nuestro modelo de negocio ha cambiado. Y si no que nos lo digan al equipo que nos encargamos de toda la gestión de suscripciones. Ahora, en lugar de gestionar suscripciones individuales, tenemos que gestionar suscripciones para un grupo de usuarios, lo que conlleva la gestión de lo que denominamos “asientos”.

Dado el aumento de la complejidad esencial del problema, decidimos refactorizar nuestro backend para tratar de reducir la complejidad accidental de nuestro código. Gracias a toda la experiencia adquirida durante estos años en cuanto a la gestión de suscripciones y en cuanto al diseño orientado al dominio, pudimos hacer una serie de mejoras que nos han simplificado la vida.

Y una de estas mejoras es justamente lo que os venimos a contar en este post. Si en tu día a día tienes que lidiar con los problemas derivados de la integración con pasarelas de pago, creemos que esto os puede interesar. Stay tuned!

Gestionando la complejidad con Domain-Driven Design

Hace ya tiempo que en Genially apostamos por utilizar un diseño orientado al dominio y arquitecturas limpias como la mejor manera de reducir la complejidad accidental en nuestro código. Creemos que es la mejor manera de desarrollar software de forma efectiva y que esté alineado con los requisitos de nuestro negocio.

El uso de los principios de Domain-Driven Design (DDD) nos ayuda a la gestión de la lógica de negocio al tener un lenguaje ubicuo que facilita la comunicación en el equipo (donde hay gente de producto y tecnología, negocio…). Además, dota nuestro código de la semántica de dominio favoreciendo una rápida detección de los problemas en la lógica de negocio y de la capacidad de adaptación al cambio de los mismos. En definitiva, lo que desde nuestro punto de vista entendemos por escribir un código de mayor calidad.

Por otro lado, la implementación de arquitecturas limpias nos permite el desacoplamiento de la lógica de negocio y la infraestructura. Este desacoplamiento nos da la independencia necesaria para ser más tolerantes al cambio y nos proporciona una mayor robustez al separar las responsabilidades en capas bien definidas y diferenciadas. Por ejemplo, podemos realizar tests de manera muy sencilla que nos facilitan el mantenimiento del código.

Sin embargo, el realizar un buen diseño del dominio junto con una arquitectura limpia no es algo sencillo. Es algo que requiere tiempo, mucha práctica y una mentalidad de aprendizaje continuo. Y es que, más importante que hacer un modelo del dominio “perfecto” a la primera, es la capacidad de iterar y mejorar ese modelo en base a la experiencia.

En nuestro caso, uno de los problemas al que nos enfrentamos a la hora de modelar la gestión de las suscripciones de Genially fue cómo integrar las pasarelas de pago (que son servicios externos) en nuestro dominio, evitando tener separada la lógica de negocio dispersa en varios puntos de nuestro código y sin acoplarnos a implementaciones concretas.

Este problema nos hizo tener que pensar en diferentes alternativas que nos ayudaran a poder gestionar de la forma más sencilla y rápida toda la comunicación con las pasarelas de pago en los numerosos casos de uso. Por supuesto, no dimos con la solución “ideal” a la primera, sino que hemos ido aprendiendo e iterando a lo largo del tiempo hasta tener una solución con la que el equipo se siente cómodo.

Integración con pasarelas de pagos

Una pasarela de pago es un servicio externo que actúa como intermediario entre comprador y vendedor, de forma que el vendedor puede recibir pagos sin acceder a los datos bancarios del comprador. Esto permite implementar un sistema de procesamiento de pagos de forma sencilla y que garantiza la seguridad en cada transacción. Las plataformas de pago que usamos en Genially a día de hoy son Stripe y Braintree.

Imagen obtenida de https://iq.opengenus.org/payment-gateway-system-design/

Integrar nuestro sistema con las pasarelas de pagos, siguiendo un modelado orientado al dominio, es uno de los mayores desafíos a los que nos hemos enfrentado en el desarrollo de toda la parte de gestión de suscripciones. Y aún más siendo un servicio crítico dentro de la empresa. Pero, ¿a qué nos referimos exactamente cuando decimos que esta integración es un desafío?

Hay que tener en cuenta que cualquier acción que se quiera realizar sobre un pago, una suscripción, un método de pago o una factura, debe realizarse a través de la pasarela de pago correspondiente (que, recordemos, es un servicio externo). Y todo lo que ocurra allí es tan importante como nuestra lógica de negocio ya que la pasarela de pagos es la que realiza la transacción y determina si algo ocurre o no finalmente.

Por lo tanto, nuestro principal objetivo era encontrar una solución que nos permitiera gestionar toda la comunicación con la pasarela de pago lo más cerca posible de nuestro dominio. Queríamos lograr así un dominio lo menos anémico y lo más cohesionado posible.

Sin embargo, que la pasarela de pago tenga tanta importancia en los casos de uso nos dificultaba este objetivo. Pensad que en prácticamente cada caso de uso se debe realizar una acción intermedia entre la validación de nuestras invariantes y la mutación del agregado que involucra la comunicación con la pasarela de pago. Esto es algo que si no diseñamos bien, nos puede llevar a tener un código donde el dominio es anémico e incluso a hacer un código difícil de mantener.

Para abordar este problema, nos planteamos varias alternativas que nos permitieran modelar de la mejor manera posible todo esto. Concretamente, valoramos la opción de delegar la comunicación con la pasarela de pago al repositorio. También valoramos la opción de que uno, o varios, servicios de dominio orquestaran esta lógica. Vamos a ver cada alternativa con un poco más de detalle.

Alternativas para modelar la comunicación con pasarelas de pagos

Delegando en el repositorio

Una de las opciones que se planteó consistía en delegar tanto la persistencia del agregado como la comunicación con la pasarela de pago en la capa de infraestructura. El razonamiento es que, si la pasarela de pago es la que finalmente decide si algo ocurre o no, podría encajar bien con delegar esta responsabilidad en el repositorio y decidir ahí si realmente el agregado se persiste o no.

Si consideramos que la conexión con la pasarela de pago no es parte del modelo de dominio, esta opción podría simplificar la capa de dominio y casos de uso. Sin embargo, esta solución oculta la complejidad de la integración con el servicio externo y nos obliga a asegurarnos de que todo funcione correctamente a nivel de infraestructura.

Simplificar la capa de aplicación y de dominio nos puede llevar a una discrepancia entre lo modelado a nivel de casos de uso y dominio, y lo que realmente ocurre en la integración con la pasarela de pago. Además hay que tener en cuenta que el concepto de “pasarela de pago” queda fuera del lenguaje ubicuo del dominio.

Orquestando con servicios de dominio

En esta otra solución partimos de la base de que tanto la lógica asociada a la comunicación con la pasarela de pago y la modificación de agregado pertenecen al modelado del dominio, al contrario que la opción anterior. Para ello, se encapsula tanto la comunicación con la pasarela de pago, como la lógica de negocio relacionada, en uno o varios servicios de dominio.

Con esta solución, si bien no tenemos toda la lógica de negocio a nivel de agregado, al menos la tenemos lo más cerca posible. Sin embargo, al no estar empujando la lógica hasta lo más profundo del dominio (el agregado), estamos creando una especie de capa intermedia que nos complicaba un poco la gestión de toda la lógica. Además nuestros agregados no son todo lo ricos que podrían ser, al contrario que los casos de uso que tienen que tener algo más de lógica.

Modelando con servicios de dominio, la opción elegida

La opción de utilizar servicios de dominio para manejar la lógica de negocio y la conexión con las pasarelas de pago es la que más nos convenció. Y es la que finalmente implementamos en nuestro proyecto.

Siempre hemos tenido como objetivo tener un modelo rico de dominio. Y con esta solución conseguimos tener una representación más fiel de las reglas de negocio en el dominio, facilitando la compresión, legibilidad y mantenimiento del código.

Servicio de dominio que encapsula la lógica de dominio y las llamadas a las pasarelas de pago

Por si fuera poco, también conseguimos que toda la orquestación y lógica relacionada con la transacción que estamos implementando quede encapsulada en un único punto: el servicio de dominio. Esto nos simplifica la gestión y el rastreo de las operaciones relacionadas con la pasarela de pagos, en lugar de tenerlo a nivel de infraestructura.

Teniendo en cuenta todo lo que hemos mencionado ya tenemos argumentos de sobra para justificar esta solución, pero aún hay más. También podemos hacer pruebas unitarias a nivel de caso de uso incluyendo la interacción con la pasarela de pagos, lo que nos da una mayor robustez, agilidad y confianza a la hora de testear esta parte.

Ejemplo de caso de uso llamando al servicio de dominio.

Sin embargo, durante la implementación de esta solución nos dimos cuenta de algunos inconvenientes que teníamos que considerar. Como casi siempre ocurre, nunca una solución es un camino de rosas.

Problema: Lógica de dominio en servicios de dominio gestionado por la capa de aplicación

Esta solución nos funcionó bien ya que pudimos implementar toda la gestión de suscripciones y pagos de Genially. Pero en nuestro afán de mejora continua, había algo que no nos terminaba de convencer en relación a cómo lo habíamos implementado.

Con la implementación que hicimos inicialmente, la lógica de verificación de la lógica de dominio y la mutación del agregado se separaban debido a la acción intermedia requerida en la comunicación con la pasarela de pagos. Esto generaba una “distorsión” en el agregado, ya que no reflejaba las reglas de negocio de manera coherente. Además, esto provocaba que quedara todo un poco inconexo.

Seguramente os estaréis preguntando qué significa eso de “distorsión e inconexión en el agregado”. Al tener una “pseudocapa” adicional para la integración con la pasarela de pago, existe la posibilidad de un mal uso tanto de los agregados como de los servicios de dominio. Es decir, la realidad es que para mutar el agregado siempre hay que usar previamente el servicio de dominio para asegurarnos de que la transacción ocurre en la pasarela de pago. Pero no hay nada que nos impida mutar el agregado sin invocar al servicio de dominio, y viceversa, porque es el servicio de aplicación el que lo orquesta.

Para ilustrar estos problemas, presentamos un ejemplo:

En este ejemplo tenemos una clase que hace referencia a un servicio de dominio encargado de gestionar las acciones que interactúan con la pasarela de pago y, al mismo tiempo, validar y mutar el agregado. Tomemos como ejemplo el caso de agregar asientos a una suscripción:

  1. Verificación de las invariantes a nivel de negocio en el agregado de suscripción. Esto nos indica si cumplimos las condiciones para agregar asientos a la suscripción.
  2. Petición a la pasarela de pago correspondiente para realizar los cambios.
  3. Mutamos el agregado llamando a un método que actualiza los asientos.

Este ejemplo refleja las problemáticas mencionadas anteriormente, ya que la lógica se encuentra dispersa entre el agregado y el servicio de dominio, lo cual dificulta la coherencia y el encapsulamiento de la lógica de negocio. Además de que nada impide que el agregado pueda ser mutado fuera de este servicio de dominio, lo que nos llevaría a un estado inconsistente de la suscripción donde la pasarela de pago y nuestra base de datos tienen un estado distinto.

Debido a estos problemas, nos vimos en la necesidad de reconsiderar la solución y buscar alternativas que nos permitieran abordar la integración con las pasarelas de pago de manera más coherente y alineada con los principios de DDD.

Solución: Agregado coordina las llamadas al servicio de dominio

Al final, queríamos conseguir que el agregado se encargara de la lógica de comunicación con la pasarela de pago y de la mutación del propio estado del agregado. Así que en lugar de usar un servicio de dominio como “pseudocapa” intermedia para coordinar esa gestión, inyectamos la abstracción de dominio de la pasarela de pago directamente en el método del agregado correspondiente. Esto nos permite poder encapsular toda la lógica de negocio en el agregado y deshacernos de esa “pseudocapa”.

A priori, esta solución nos puede parecer extraña por el hecho de inyectar un servicio de dominio en un método del agregado. Pero lo cierto es que, si lo pensamos bien, esta dependencia indica que, para llevar a cabo esa acción, se necesita forzosamente el servicio de dominio encargado de la comunicación con la pasarela de pago. Por lo tanto, conseguimos las dos cosas que buscábamos: 1) que el agregado contemple toda la lógica necesaria para realizar la acción de dominio, y 2) que no se pueda mutar el agregado sin el servicio de dominio, evitando las inconsistencias comentadas con anterioridad.

Ejemplo de caso de uso tras el refactor

Aquí tenemos un ejemplo de cómo quedaría un caso de uso tras el refactor. Realmente a nivel de código no se nota demasiado la diferencia, más que el hecho de que en lugar de invocar a un servicio de dominio para manejar la lógica que conlleva esa acción, invocamos directamente al método del agregado subscription.addSeats que mostramos en el siguiente fragmento de código:

Ejemplo de agregado tras el refactor

Tomando como ejemplo el método addSeats del agregado Subscription, podemos observar cómo ahora esta lógica está integrada y encapsulada en un solo lugar, evitando separaciones y mejorando la cohesión.

En definitiva, las ventajas que obtuvimos con esta solución fueron las siguientes:

  1. Encapsulación completa: Toda la lógica de dominio y la comunicación con la pasarela de pago se encuentra dentro del método del agregado. No hay necesidad de separar la validación de los datos y la llamada a la pasarela de pago en métodos distintos como en la solución anterior con los servicios de dominio.
  2. Evitar confusiones: Al tener la lógica encapsulada en el método del agregado, se evitan confusiones o errores al invocar directamente el método del agregado en lugar de llamar al servicio de dominio correspondiente. Esto garantiza que se sigan los flujos adecuados y se realicen todas las acciones necesarias.
  3. Eliminación de métodos de validación públicos: Al no requerir validaciones externas al agregado antes de llamar a la pasarela de pago, no es necesario exponer públicamente los métodos de validación del agregado. Esto mejora la encapsulación y evita que se realicen validaciones incorrectas o inadecuadas fuera del agregado.
  4. Trazabilidad y legibilidad del código: Al tener toda la lógica en un solo lugar, el código se vuelve más legible y fácil de seguir. Es más sencillo comprender qué acciones se realizan y en qué orden, lo que facilita el mantenimiento y la comprensión del sistema.
  5. Simplificación de la experiencia de desarrollo: Al seguir los principios de DDD y arquitecturas limpias se simplifica en gran medida la experiencia de desarrollo. No es necesario tener en cuenta reglas internas complejas, ya que se sigue un patrón claro y se trabajan dentro de los límites del agregado.

Conclusiones

Como habréis podido apreciar, el proceso hasta llegar a una solución que nos convenciera ha sido largo, complejo y muy debatido dentro del equipo de Genially. Y hay que tener en cuenta que se ha ido pensando y decidiendo sobre la marcha, con un proyecto ya en producción, funcionando y con la necesidad de ir refactorizando poco a poco.

En resumen, utilizar DDD en la gestión de suscripciones y pagos de Genially nos ha proporcionado una serie de beneficios, como una mejor gestión de la lógica de negocio, separación de la lógica de negocio y la infraestructura, encapsulación de la lógica en un solo punto y facilidad de comunicación con otras APIs. Estos beneficios nos han permitido tener un diseño más robusto y cercano al dominio, mejorando así la calidad y confiabilidad de nuestro sistema.

--

--

Backend developer at @genially. Formerly at @footters. You can follow me on @alexismchacon.