Skip to content

Instantly share code, notes, and snippets.

@aleixmorgadas
Last active June 6, 2024 14:10
Show Gist options
  • Save aleixmorgadas/cb1322d80e600115dd657d47cd767135 to your computer and use it in GitHub Desktop.
Save aleixmorgadas/cb1322d80e600115dd657d47cd767135 to your computer and use it in GitHub Desktop.

Invariantes vs Reglas de negocio

A partir de esta conversación escribo este Gist.

Posible caso

Tenemos un almacen que tiene un stock por producto.

Regla de negocio:

  • No podemos vender un producto que no tenemos stock.

En este caso podríamos pensar que esto es una invariante de stock, no puede existir un stock negativo. Por lo que creamos el concepto:

class Stock {
  constructor(private readonly productId: string,
              private readonly stock: number) {
    if (this.stock < 0) throw new Error('stock cannot be negative');            
  }              
}

Hemos creado una invariante en un objeto de dominio. Esto es muy común en DDD de intentar representar las invariantes a nivel de Value Object, Entity, o Aggregate.

Pero, ¿realmente representa el dominio?

No. El concepto de Stock puede pasar muchas cosas:

  • Se ha perdido en el almacen el articulo.
  • Se ha roto y no se había reportado.
  • Dos personas han comprado el mismo articulo a la vez.
  • Que entre se ha hecho el pedido y se ha confirmado se ha queado sin stock.

Hay un montón de casos que hacen que un stock pueda ser negativo temporalmente, de ahí la consistencia eventual.

Lo que dice la regla de negocio es que en el momento de compra queremos que el stock sea positivo. Por lo que lo que podemos hacer es tener una regla de negocio que sea en el domain service estilo:

Dream code :pointdown:

class OrderService {
  placeOrder(order) {
     const stockForProduct = stock.for(order.product);
     if (stockForProduct < 0) throw new Error('stock cannot be negative'); 
  }
}

En el segundo caso, hemos implementado la regla de negocio pero permitiendo que el stock sea negativo para poder implementar entonces esa logica de negocio para esos casos. Estilo:

  • Si stock es menor que 0, entonces pedir con urgencia a tales proveedores y entregarlo con máxima priodidad y dar un vale de regalo por las molestias.

En el primer caso, el objeto Stock no puede representar stock negativo porque es a nivel de invariante de objeto de dominio. En el segundo si que permitimos esa representación del objeto y podemos modelar el dominio de forma menos rigida.

@OscarGalindo
Copy link

OscarGalindo commented Jun 5, 2024

Buenas! Gran ejemplo :) como siempre

Quiero comentar algo que veo a ver que opinas:

  • Se ha perdido en el almacen el articulo. -> Esto implicaría que en stock tenemos 1 pero en realidad no existe.
  • Se ha roto y no se había reportado. -> El mismo caso que en el primero
  • Dos personas han comprado el mismo articulo a la vez. -> Problema de transaccionalidad
  • Que entre se ha hecho el pedido y se ha confirmado se ha queado sin stock. -> Stock sigue sin ser negativo

Creo que no puedes tener stock negativo, a no ser que el 3er punto no lo tengas solventado. Puedes tener el stock mal identificado en tu sistema, es algo común por lo que he ido viendo además, el stock en tu dominio se debe sincronizar con el stock del almacen real, y a su vez ese stock del almacen real debe comprobarse de vez en cuando (tenemos 1200 camisetas y en realidad hay solo 1100).

Sumo otro problema que me gustó mucho cuando lo escuché, y es que haces un pedido, se reserva el stock para ti, el almacen lo envía genial y por el camino (suele ser por zonas del valles normalmente :P ), se "cae del camion". Lo que yo entiendo de todo esto, no es dejar de hacer comprobaciones en nuestro dominio, sino también tener en cuenta otras casuísticas muy reales como las que comentas.

En el comando de añadir a carrito se ve algo mas claro la comprobación del stock por ejemplo, la condición de carrera que hay comprobandolo, pero Udi Dahan por ejemplo comentaba que una posible solución es "caducar" el carrito cada X tiempo.

Respecto a tu solución, cuando haces un pedido, no creo que la acción sea preguntar por el stock sino mas bien "hacer una reserva", moviendo así la lógica de stock disponible al aggregate, por ejemplo:

var reservedSku = stock.reserveFor(order.id)

No se si hemos solucionado el tercer punto con esto? ya sería imposible que alguien reserve el mismo producto dos veces en dos transacciones diferentes

Entiendo el punto que quieres comentar, Udi Dahan habla mucho sobre estos temas y sobre la flexibilidad que debe tener nuestro sistema

A ver que opinas y como seguimos!

Muchas gracias Aleix!

@aleixmorgadas
Copy link
Author

aleixmorgadas commented Jun 5, 2024

Hola Oscar!

Voy a dividir mi respuesta en dos puntos.

  1. Como modelar cuando no sabemos, y hay mucho que no sabemos que no sabemos (unknown-unknowns)

Los puntos que comentas justamente se deben a que tienes un mayor conocimiento del dominio y puedes modelarlo con más intencionalidad.

Mi punto es para ese dominio que desconoces, ese que necesitas que te dé la flexibilidad del aprendizaje sin dejarte atado a unas decisiones de diseño prematuras que son cuando menos conoces del dominio. Y no solo un aprendizaje como desarrollador, sino también de parte de las personas de negocio o expertos de dominio. Tenemos que asumir que todas las personas tienen que aprender y no modelar para el conocimiento actual del problema, sino modelar pensando en ir añadiendo esos aprendizajes que descubrimos según entregamos valor.

De ahí ser precavido y modelar sin crear una solución rígida que permita añadir ese conocimiento. Por eso me centro en que lo importante es modelar las reglas de negocio y no asumir que lo que sabemos hoy que podrían ser invariantes lo serán mañana.

En la solución que he propuesto inicialmente se ve claramente que podría haber varios bounded context, pero esto lo sabemos porque el caso es conocido y bien entendido. Pero si fuéramos los primeros en hacer una tienda online, seguro que tendríamos que aprender todas estas cosas y tendríamos un dominio acoplado porque tendríamos que descubrir esos subdominios según aprendemos.

  1. Las propuestas de modelar este caso

Vale la pena decir que nunca he modelado un sistema de stock en mi vida, y que lo que comentaré es basado en mi experiencia y hacia donde tiraría.

Creo que no puedes tener stock negativo, a no ser que el 3er punto no lo tengas solventado. Puedes tener el stock mal identificado en tu sistema, es algo común por lo que he ido viendo además, el stock en tu dominio se debe sincronizar con el stock del almacen real, y a su vez ese stock del almacen real debe comprobarse de vez en cuando (tenemos 1200 camisetas y en realidad hay solo 1100).

Aquí justamente incluyes que hay una consistencia eventual entre esos dos sistemas, y tenemos que tener eso en cuenta. Aquí añades el stock que aparece en nuestro sistema y el real tienen que sincronizarse y habrá N procesos según casuísticas. Otra vez, aquí en vez de tener una invariante tenemos reglas de negocio con casuísticas muy complejas.

Sumo otro problema que me gustó mucho cuando lo escuché, y es que haces un pedido, se reserva el stock para ti, el almacen lo envía genial y por el camino (suele ser por zonas del valles normalmente :P ), se "cae del camion". Lo que yo entiendo de todo esto, no es dejar de hacer comprobaciones en nuestro dominio, sino también tener en cuenta otras casuísticas muy reales como las que comentas.

💯 Siempre hay que modelar lo que sabemos, si no viviríamos en el caos (o en un mayor caos si es que el orden existe) 😂

Respecto a tu solución, cuando haces un pedido, no creo que la acción sea preguntar por el stock sino mas bien "hacer una reserva", moviendo así la lógica de stock disponible al aggregate, por ejemplo:

var reservedSku = stock.reserveFor(order.id)

No se si hemos solucionado el tercer punto con esto? ya sería imposible que alguien reserve el mismo producto dos veces en dos transacciones diferentes

Este modelado parece que tiene sentido, pero creo que ese aggregate es un cuello de botella si tiene que ser consistente en entregarte reservas igualmente.

Si haces ese stock transaccional, podrías tener problemas de rendimiento, ¿no? A no ser que no compruebes nada en el agregate, por lo que no haría falta usar el aggregate de stock, sino crear una entidad de Reservation donde esta se confirma luego.

@aleixmorgadas
Copy link
Author

Sobre var reservedSku = stock.reserveFor(order.id) esta operación me ha dejado pensando mucho, y no estoy seguro si lo modelaría así para sistemas con mucha concurrencia.

También es verdad que la mayoría de artículos no se compran y son unos pocos los que reciben el mayor trafico.

Por lo que el número de artículos es mucho mayor a los artículos que se muestran, que es mucho mayor a los que se terminan comprando.

De ahí seguramente acabaríamos con dos tipos de procesadores de pedidos, los de alta frequencia y los de baja (si es que esta diferencia tiene sentido).

Luego, jugaría con las Terms and Conditions para permitir retrasos, y en caso de que un articulo no este disponible haría cosas a nivel de negocio estilo:

  • Busca el articulo en el almacen más cercano y enviarlo.
  • Dale un descuento en la siguiente compra por las molestias.
  • Y otro sin fin de procesos que ayuden para esos casos que nos quedamos sin stock si este es lo suficientemente poco frequencte para que el coste no se coma el beneficio.

@K4L1Ma
Copy link

K4L1Ma commented Jun 5, 2024

Handling stock reservations in high-concurrency environments requires careful architectural considerations. Amazon’s approach involves using message queues to process reservations sequentially, which effectively prevents race conditions. This, combined with optimistic locking, ensures data integrity and efficiently manages concurrent updates, allowing for scalable, reliable stock management.

Ensuring stock levels do not go negative is invariant, as negative stock would indicate a fundamental system error or these IRL edge cases being mentioned. Strategies for managing stock shortages, like checking nearby warehouses or offering alternatives, are business rules that enhance the system’s robustness and user experience.

@aleixmorgadas
Copy link
Author

Handling stock reservations in high-concurrency environments requires careful architectural considerations. Amazon’s approach involves using message queues to process reservations sequentially, which effectively prevents race conditions. This, combined with optimistic locking, ensures data integrity and efficiently manages concurrent updates, allowing for scalable, reliable stock management.

Ensuring stock levels do not go negative is invariant, as negative stock would indicate a fundamental system error or these IRL edge cases being mentioned. Strategies for managing stock shortages, like checking nearby warehouses or offering alternatives, are business rules that enhance the system’s robustness and user experience.

Lo bueno es como Amazon ha llegado a este sistema a lo largo de muchos años de aprendizaje. Lo importante de la discusión es como crear sistemas que permitan el aprendizaje, no tanto el caso de uso que me he inventado.

Justamente Amazon tiene casi 30 años de experiencia, por lo que lo interesante es como tomaron las decisiones al principio y fueron adaptando el software según aprendían. Me imagino que su primer black friday llevo tantos aprendizajes que repensaron muchos dominios.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment