Where Should a Shopping Cart Live in a Multi-Device World?
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.