Developing Shopping Cart for E-commerce
Shopping cart — central component of any e-commerce project. Major abandonment happens here: awkward quantity management, loss of items on page reload, session conflicts for authorized users. Building cart from scratch takes 3–6 business days depending on business logic complexity.
Cart Storage Architecture
Cart exists in three states: guest (anonymous), user (account-linked), merged (merge on login). Guest data stored in localStorage or sessionStorage — policy choice. On auth client-server merge must resolve conflicts: e.g. same product in both — sum quantities or take max.
Server-side cart table:
CREATE TABLE cart_items (
id BIGSERIAL PRIMARY KEY,
cart_id UUID NOT NULL,
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
session_id VARCHAR(255),
product_id BIGINT NOT NULL,
variant_id BIGINT,
quantity INT NOT NULL DEFAULT 1,
price_snapshot NUMERIC(12,2) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_cart_items_cart_id ON cart_items(cart_id);
CREATE INDEX idx_cart_items_user_id ON cart_items(user_id);
price_snapshot locks price at add time — critical for timed sales and dynamic pricing.
Quantity Update Logic
Changes should be optimistic: UI updates instantly, request sent in background. On error — rollback with notification. React Query implementation:
const updateQuantity = useMutation({
mutationFn: ({ itemId, qty }: { itemId: number; qty: number }) =>
api.patch(`/cart/items/${itemId}`, { quantity: qty }),
onMutate: async ({ itemId, qty }) => {
await queryClient.cancelQueries({ queryKey: ['cart'] });
const prev = queryClient.getQueryData(['cart']);
queryClient.setQueryData(['cart'], (old: Cart) => ({
...old,
items: old.items.map(i => i.id === itemId ? { ...i, quantity: qty } : i),
}));
return { prev };
},
onError: (_, __, ctx) => {
queryClient.setQueryData(['cart'], ctx?.prev);
toast.error('Could not update quantity');
},
});
Min/max quantities set at product level: min_order_qty, max_order_qty. If 3 units left in stock, "+" button blocks at that number.
Availability Check and Reservation
Decision on add: make soft reserve (reduce available stock) or not. Soft reserve reduces competition but creates "dead" reserves from abandoned carts. Compromise — reserve only on checkout init, keep cart informational.
When displaying cart check current stock via API. If out of stock — show warning on that line item, don't block whole cart.
Cart Total Calculation
Cart total includes layers:
| Component | Logic |
|---|---|
| Subtotal | Sum of price_snapshot × quantity for all items |
| Discounts | Applied by priority: promotional > coupon > loyalty |
| Shipping | Preliminary estimate, exact on checkout |
| VAT | Included in price or separate (depends on config) |
Calculation done on server per cart change. Client gets ready sums — no browser math.
Mini-Cart and Full Page
Mini-cart in header (dropdown or sidebar) shows last 5 items, count, "Checkout" button. Full page /cart shows all items with edit capability. Both subscribe to same state — React Query or Zustand.
Important: header count updates via Server-Sent Events or polling every 30s — relevant if user works in multiple tabs.
Cart Persistence
Guest cart saved 30 days in cookie (cart_id). On login — merge and move to DB. If authorized user logs out — cart stays in DB, restored on re-login.
Authorized user cart syncs across devices — key difference from guest cart.
Cart Analytics
All cart events → analytics: add_to_cart, remove_from_cart, view_cart. GA4 standard e-commerce events with item_id, item_name, price, quantity. Yandex.Metrica — similar structure via ym(id, 'reachGoal', 'cart_add', {...}).
Abandoned carts tracked separately: if user added items but didn't checkout for N hours — trigger email sequence.
Common Implementation Issues
-
Race condition on simultaneous add: solved via
SELECT FOR UPDATEat DB level or idempotency key in API - Price changed after add: show notification, auto-recalculate
- Product discontinued: block checkout, offer remove
- VAT for different countries: determine by IP/shipping address, apply correct rate







