¿Dónde vive un carrito cuando el usuario usa más de un dispositivo?

Lucas Semelin
8 min lectura
#Cloudflare #Durable Objects #e-commerce #WebSockets #arquitectura de software #Laravel

¿Dónde vive un carrito cuando el usuario usa más de un dispositivo?

Hace unos días estaba utilizando el e-commerce de una empresa bastante conocida desde su aplicación móvil.

Agregué dos productos al carrito y cerré la aplicación.

Al día siguiente estaba trabajando con la computadora y entré al mismo e-commerce desde la web.

El carrito estaba vacío.

Por curiosidad volví a abrir la aplicación móvil.

Los dos productos seguían ahí.

Mismo usuario.
Mismo e-commerce.
Dos dispositivos.
Dos carritos diferentes.

El problema no era que el carrito no se hubiera persistido. De hecho, se había persistido perfectamente.

El problema era dónde se había persistido.

Cuando el carrito pertenece al dispositivo

Durante mucho tiempo fue muy habitual guardar el estado de un carrito localmente.

En una aplicación web puede ser localStorage, cookies o algún otro mecanismo de almacenamiento del navegador.

En una aplicación móvil puede ser una base de datos local o almacenamiento propio de la aplicación.

Esto permite resolver algo fundamental: que si el usuario cierra la aplicación y vuelve unas horas después, sus productos continúen ahí.

El problema aparece cuando pensamos la experiencia desde otro lugar.

Hoy no necesariamente empezamos y terminamos una compra en el mismo dispositivo.

Podemos descubrir un producto desde el celular mientras viajamos, agregarlo al carrito y, algunas horas después, continuar la compra desde una computadora.

O empezar desde la web del trabajo y terminarla más tarde desde el teléfono.

Desde el punto de vista del usuario no existen dos carritos.

Existe su carrito.

Y espera que lo acompañe.

El estado ya no debería pertenecer al dispositivo

En ese escenario, guardar el carrito solamente de manera local deja de ser suficiente.

El estado necesita vivir en algún lugar compartido.

La solución tradicional es relativamente directa:

Mobile ───┐
          │
Web ──────┼── API ── Backend ── Database / Redis
          │
Tablet ───┘

Cuando una aplicación modifica el carrito, persiste ese cambio en el backend.

Cuando otro dispositivo consulta el carrito, obtiene el mismo estado.

Esto ya resuelve gran parte del problema.

Pero podemos avanzar un paso más.

¿Qué pasa si tenemos dos dispositivos abiertos simultáneamente?

Notebook                         Mobile
    │                               │
    │  carrito: 2 productos         │
    │                               │
    │                    agrega producto C
    │                               │
    │                               ▼
    │                            Backend
    │                               │
    │                               ▼
    │                         carrito actualizado
    │
    ?

La notebook sigue mostrando un carrito desactualizado hasta que vuelva a consultar al servidor, recargue la página o implementemos algún mecanismo adicional de sincronización.

Podemos resolverlo con polling.

Podemos utilizar WebSockets.

Podemos emitir eventos.

Hay varias arquitecturas posibles.

Pero acá aparece una alternativa que me parece particularmente interesante para este tipo de problema: Cloudflare Durable Objects.

Un objeto por carrito

Una forma de pensar Durable Objects es como pequeñas unidades de compute con identidad y estado propio.

Cada instancia tiene un identificador único, almacenamiento privado y puede actuar como punto de coordinación para distintos clientes que necesitan trabajar sobre el mismo estado. Cloudflare recomienda modelarlos alrededor del “átomo de coordinación” de una aplicación: una sala, un documento, una sesión, un workspace o cualquier unidad que necesite coordinar múltiples clientes.

Para un e-commerce, ese objeto podría ser el carrito.

cart:84721

Y varios dispositivos podrían conectarse al mismo objeto:

                  ┌─────────────────┐
                  │    cart:84721   │
                  │                 │
Mobile ──────────►│                 │◄────────── Web
                  │                 │
Tablet ──────────►│                 │
                  └─────────────────┘

Ese Durable Object puede coordinar el estado activo del carrito y las conexiones WebSocket asociadas.

Supongamos que desde el celular agregamos un producto.

Mobile
   │
   │ add item
   ▼
cart:84721
   │
   ├── actualiza estado
   │
   ├── persiste cambio
   │
   └── broadcast
             │
             ▼
           Web

La sesión abierta en la computadora puede actualizarse inmediatamente.

Sin polling.

Sin esperar una recarga.

Y, sobre todo, ambos dispositivos están interactuando con el mismo carrito.

Durable Objects no son solamente WebSockets

Esto es lo que me parece más interesante de este enfoque.

Si solamente necesitáramos enviar una notificación diciendo:

el carrito cambió

podríamos utilizar Laravel Reverb, Pusher, Ably o prácticamente cualquier infraestructura WebSocket.

Durable Objects plantean algo diferente.

El objeto puede representar:

este es el carrito y este es su estado actual.

Cloudflare define cada Durable Object como una instancia globalmente identificable con almacenamiento propio. Su almacenamiento SQLite es transaccional, fuertemente consistente y privado de esa instancia.

Conceptualmente podemos tener:

Cart 84721

items
────────────────────────
product     qty
A            2
B            1
C            3

coupon
────────────────────────
WELCOME10

connections
────────────────────────
mobile
web

Esto convierte al carrito en una unidad independiente de coordinación.

No significa necesariamente que tengamos que mover toda la lógica del e-commerce hacia Cloudflare.

De hecho, probablemente no lo haría.

PostgreSQL seguiría siendo la fuente de verdad

Si estuviera construyendo este sistema con Laravel, mantendría el dominio principal en Laravel + PostgreSQL.

Laravel
│
├── customers
├── products
├── prices
├── inventory
├── orders
├── payments
└── PostgreSQL

El checkout, las órdenes, los pagos y la consistencia definitiva del stock seguirían pasando por ahí.

Durable Objects tendría otro rol:

Durable Object
│
├── estado activo del carrito
├── sincronización
├── conexiones WebSocket
├── cambios de cantidades
├── disponibilidad visible
└── coordinación entre clientes

La arquitectura podría verse aproximadamente así:

Mobile                        Web
   │                            │
   └──────── WebSocket ─────────┘
                │
                ▼
        Cloudflare Worker
                │
                ▼
        Durable Object
          cart:84721
                │
                │ API / events
                ▼
             Laravel
                │
                ▼
           PostgreSQL

PostgreSQL continúa siendo la fuente de verdad del negocio.

El Durable Object representa el estado activo y compartido de la experiencia.

¿Y si el usuario cierra todos los dispositivos?

Ahí aparece otra característica interesante: WebSocket Hibernation.

Cloudflare permite que las conexiones WebSocket continúen conectadas a su red mientras el Durable Object deja de permanecer activo en memoria. Cuando llega un nuevo mensaje, el objeto vuelve a inicializarse y atiende el evento. Durante los períodos en los que puede hibernar no acumula duración facturable.

Esto es especialmente interesante para un e-commerce.

Un carrito puede existir durante horas o días pero tener muy poca actividad.

Por ejemplo:

11:43  agrega producto
11:45  cambia cantidad

       3 horas sin actividad

14:52  abre carrito
14:54  elimina producto

       6 horas sin actividad

21:03  checkout

No tiene demasiado sentido mantener infraestructura activa durante todo ese período exclusivamente para ese carrito.

El modelo serverless de Durable Objects se adapta bastante bien a este patrón.

Además, actualmente están disponibles tanto en Workers Free como Paid. El plan gratuito incluye, entre otros límites, 100.000 requests diarios, 13.000 GB-s diarios y almacenamiento SQLite, lo que permite experimentar con esta arquitectura sin necesitar inicialmente infraestructura dedicada.

El carrito también puede ser algo más que individual

Una vez que el carrito deja de pertenecer al dispositivo y pasa a ser una entidad compartida, aparecen casos interesantes.

Por ejemplo, un carrito colaborativo.

Compra oficina

Juan ─────┐
          │
María ────┼──── cart:company:238
          │
Pedro ────┘

Varias personas podrían preparar una misma compra simultáneamente.

También podría existir interacción entre comprador y vendedor.

Cliente
   │
   │ solicita cotización
   ▼
carrito
   ▲
   │ ajusta precio / disponibilidad
   │
Vendedor

O reservar temporalmente determinados productos mientras el usuario completa una operación.

En todos esos escenarios el carrito empieza a parecerse menos a una colección de filas en una tabla y más a una sesión activa en la que distintos actores interactúan con un mismo estado.

Y ese es precisamente el tipo de problema para el que Durable Objects resulta interesante.

¿Usaría Durable Objects para cualquier carrito?

No.

Para un e-commerce convencional:

agregar producto
editar cantidad
eliminar producto
checkout

probablemente seguiría utilizando simplemente Laravel, PostgreSQL o Redis.

Es más sencillo.

Tiene menos piezas.

Y resuelve perfectamente el problema.

Introducir infraestructura distribuida solamente porque está disponible suele agregar complejidad sin aportar suficiente valor.

Pero la decisión cambia cuando aparecen requisitos como:

  • sincronización inmediata entre mobile y web;
  • múltiples sesiones simultáneas;
  • carritos colaborativos;
  • interacción cliente-vendedor;
  • disponibilidad que cambia en tiempo real;
  • reservas temporales;
  • pricing dinámico;
  • otros procesos actuando sobre el mismo carrito.

En esos casos ya no estamos resolviendo únicamente persistencia.

Estamos resolviendo coordinación de estado.

El problema no es guardar el carrito

Volviendo a la experiencia con la que empezó este artículo:

los productos estaban guardados.

No se había perdido información.

El problema era que el sistema había entendido el carrito como parte de la aplicación instalada en mi teléfono.

Yo, en cambio, lo entendía como parte de mi relación con ese e-commerce.

Y esa diferencia parece pequeña, pero arquitectónicamente cambia bastante las cosas.

Cuando diseñamos software para personas que se mueven continuamente entre aplicaciones, navegadores y dispositivos, quizás la pregunta ya no debería ser:

¿Cómo persistimos el carrito?

Sino:

¿Dónde debería vivir el estado del carrito?

LocalStorage puede representar perfectamente el estado de un navegador.

Una base de datos puede representar perfectamente el estado persistente del negocio.

Y tecnologías como Durable Objects abren una tercera posibilidad interesante:

representar el estado activo y compartido de una experiencia.

Compartir:

Disponible para nuevos proyectos

¿Tenés una idea o un problema que podría resolverse mejor con software?

Contame qué querés construir o qué necesitás resolver.

Podemos empezar por entender el problema, explorar las posibilidades y definir el próximo paso.