Where Should a Shopping Cart Live in a Multi-Device World?

Lucas Semelin
7 min lectura
#Cloudflare #Durable Objects #e-commerce #WebSockets #software architecture #Laravel

Where Should a Shopping Cart Live in a Multi-Device World?

A few days ago, I was shopping through the mobile app of a fairly well-known retailer.

I added two products to my cart and closed the app.

The next day, I was on my laptop, so I opened the same store on the web.

The cart was empty.

I checked the mobile app again.

The products were still there.

Same account.
Same store.
Different device.
Different cart.

Nothing had actually been lost.

The cart had been persisted just fine.

The real issue was where that state lived.

When the cart belongs to the device

For years, storing cart state locally has been a perfectly reasonable approach.

On the web, that might mean localStorage, cookies, or some other kind of browser storage.

On mobile, it could be a local database or the app's own persistent storage.

That solves an important problem: if the user closes the app and comes back later, the cart is still there.

But that model assumes something that is becoming less and less true:

that the user will continue the experience on the same device.

Today, people constantly move between devices.

You might discover a product on your phone, add it to the cart, and complete the purchase later from your laptop.

Or start shopping from a desktop at work and finish the order from your phone at home.

From the user's perspective, there are not multiple carts.

There is just their cart.

And they expect it to follow them.

The state should not belong to the device

Once you look at it that way, local persistence is no longer enough.

The cart needs to live somewhere shared.

A common architecture is straightforward:

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

When one client changes the cart, the backend stores the new state.

When another client loads the cart, it gets the same data.

That already solves most of the problem.

But there is another question.

What happens when the user has two devices open at the same time?

Laptop                           Mobile
   │                                │
   │  cart: 2 products              │
   │                                │
   │                    adds product C
   │                                │
   │                                ▼
   │                             Backend
   │                                │
   │                                ▼
   │                         cart updated
   │
   ?

The laptop is now out of sync.

It will stay that way until it polls the server, reloads the cart, or receives some kind of real-time event.

There are plenty of ways to solve that.

Polling.

WebSockets.

Pub/sub.

Event-driven architectures.

But there is one model I find particularly interesting for this type of problem:

Cloudflare Durable Objects.

What if the cart were an object?

One useful way to think about a Durable Object is as a small, stateful compute unit with a stable identity.

Instead of thinking only in terms of servers, database rows, and events, you can model a specific thing in your system as an active object.

For example:

cart:84721

That object can represent one specific shopping cart.

And every device using that cart can connect to the same object.

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

Now imagine the user adds an item from their phone.

Mobile
   │
   │ add item
   ▼
cart:84721
   │
   ├── update state
   │
   ├── persist change
   │
   └── broadcast
             │
             ▼
            Web

The web session can update immediately.

No polling.

No manual refresh.

Both clients are talking to the same cart.

This is more than a WebSocket problem

At first glance, this might sound like something you could solve with any WebSocket server.

And that is true, up to a point.

If all you need is:

"the cart changed"

then Laravel Reverb, Pusher, Ably, or another real-time service can handle that perfectly well.

Durable Objects become interesting when the object itself can represent:

"this is the cart, and this is its current state."

Conceptually:

Cart 84721

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

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

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

Now the cart is no longer just data stored somewhere plus events flying around the system.

It becomes a coordination point.

That is a subtle difference, but an important one.

I would still keep Laravel and PostgreSQL

I would not move the entire e-commerce domain into Durable Objects.

If I were building this with Laravel, I would still keep the core business logic in Laravel and PostgreSQL.

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

Checkout, payments, orders, and final inventory consistency would stay there.

Durable Objects would have a narrower responsibility:

Durable Object
│
├── active cart state
├── synchronization
├── WebSocket connections
├── quantity changes
├── visible availability
└── client coordination

The overall architecture might look something like this:

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

PostgreSQL remains the source of truth for the business.

The Durable Object manages the active, shared state of the experience.

Why hibernation matters

There is another part of the Durable Objects model that fits shopping carts particularly well: WebSocket Hibernation.

A cart may exist for hours or even days, while being active only for a few seconds at a time.

A typical session might look like this:

11:43  add product
11:45  change quantity

       3 hours of inactivity

14:52  open cart
14:54  remove product

       6 hours of inactivity

21:03  checkout

Keeping dedicated infrastructure active during all that idle time would not make much sense.

With WebSocket Hibernation, the connection can stay open while the Durable Object itself does not need to remain actively running the entire time.

That makes the model especially attractive for systems with lots of mostly-idle real-time sessions.

The cart can become collaborative

Once the cart becomes a shared object instead of device-local state, other possibilities start to appear.

For example, a shared cart for a company purchase:

Office purchase

John ─────┐
          │
Maria ────┼──── cart:company:238
          │
Peter ────┘

Several people could add or remove products at the same time.

Or imagine a B2B workflow where a buyer and a salesperson interact around the same cart.

Customer
   │
   │ requests quote
   ▼
cart
   ▲
   │ adjusts price / availability
   │
Salesperson

You could also use the same model for temporary stock reservations, dynamic pricing, or approval workflows.

At that point, the cart starts to look less like a database record and more like an active session shared by multiple actors.

That is where Durable Objects become much more compelling.

Would I use Durable Objects for every cart?

No.

For a conventional shopping cart:

add product
change quantity
remove product
checkout

Laravel with PostgreSQL or Redis is probably enough.

It is simpler.

There are fewer moving parts.

And it solves the problem well.

Adding distributed infrastructure just because it is available is usually a bad trade.

But the equation changes when the requirements include things like:

  • real-time sync between mobile and web;
  • multiple active sessions;
  • shared or collaborative carts;
  • buyer and seller interaction;
  • live availability;
  • temporary stock reservations;
  • dynamic pricing;
  • multiple processes acting on the same cart.

At that point, the problem is no longer just persistence.

It is state coordination.

The real question is not how to save the cart

Going back to the experience that triggered this thought:

the products had not disappeared.

Nothing had been lost.

The system had simply treated the cart as something that belonged to the app installed on my phone.

I was thinking about it differently.

To me, the cart belonged to my account and my relationship with that store.

That sounds like a small distinction.

Architecturally, it is not.

When users constantly move between apps, browsers, and devices, the question may no longer be:

How should we persist the cart?

A better question might be:

Where should the cart state live?

localStorage is great at representing the state of a browser.

A database is great at representing persistent business state.

And technologies like Durable Objects introduce another interesting model:

a place to represent the active, shared state of an experience.

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.