One of the more consequential early architectural decisions we made was recognizing that a storefront and a checkout are not the same UI problem. They look adjacent — they are adjacent, in the user flow — but they have different data models, different performance requirements, different failure modes, and different interaction patterns.
shop.js was born from this recognition. It was the storefront layer: product catalog, search, filtering, product detail pages, cart state management. checkout.js handled everything downstream of "proceed to checkout." The boundary between them was the cart object.
The Product Catalog Problem
A product catalog in 2010 was a surprisingly hard rendering problem. Clients had product sets ranging from dozens to tens of thousands of items. The rendering approach that worked for a 50-product boutique failed completely for a 10,000 SKU catalog.
shop.js used virtual pagination from the start. The catalog endpoint returned pages of products. shop.js rendered one page, tracked which page you were on, and pre-fetched the next page on scroll — a simplified version of what would later be called infinite scroll, though we called it "progressive loading" at the time. Backbone.js Collection models ended up being a good fit here when we integrated with Backbone a year later; in 2010 we rolled our own observable collection.
Filtering was the other hard problem. Client-side filtering against a pre-loaded product set worked for small catalogs. For larger catalogs, filters had to hit the API. shop.js abstracted this: it exposed the same filtering interface to the UI layer regardless of whether filtering happened client-side or server-side, switching behavior based on catalog size.
Cart State as the Boundary Contract
The cleanest decision we made in the shop.js architecture was defining the cart object as the formal interface between shop.js and checkout.js. shop.js owned cart state — it managed add-to-cart, quantity updates, item removal. checkout.js consumed a cart snapshot at the moment the user initiated checkout.
This meant checkout.js was a pure function in the architectural sense: given a cart and a user session, produce an order. It did not need to understand how the cart had been built. shop.js did not need to understand payment flows or address validation.
The cart object was serializable JSON. It could be persisted to localStorage for anonymous sessions or to the Hanzo API for authenticated users. It could be reconstructed from either source on page load. The serialization format was versioned so that carts created by an older version of shop.js could be read by a newer one.
What shop.js Did Not Handle
Equally important to what shop.js did was what it explicitly refused to do:
- No payment logic. shop.js had no knowledge of payment methods, card tokens, or gateway APIs. That was checkout.js's domain.
- No user authentication. shop.js treated sessions as opaque identifiers. Authentication was a separate service.
- No analytics. shop.js emitted events (product viewed, item added to cart, cart updated) but did not know what happened to those events. An analytics integration could listen and forward them. The separation meant you could swap analytics providers without touching shop.js.
The event emission pattern was influenced by the pubsub work we were doing in the backend at the time. Everything significant that happened in shop.js emitted a named event with a structured payload. External code could observe those events without coupling to shop.js internals.
The Separation in Practice
The shop.js / checkout.js split turned out to be the right call for another reason we had not fully anticipated: different teams owned different parts of the UI. A client might have their designers build the storefront while their engineers handled checkout, or vice versa. The separation aligned with how product teams actually organized.
A clear interface between two libraries made parallel development possible. You could build and test shop.js independently, stub the checkout boundary in tests, and be confident the integration would work. That kind of testability was not common in JavaScript commerce tooling in 2010.
Read more
Hanzo.js: A JavaScript SDK Designed for Commerce
In 2012 JavaScript was becoming a real application platform. We shipped a commerce SDK built for the modern web — before 'modern web' was a phrase.
Backbone.js and the Commerce SDK: MVC Comes to the Storefront
How we integrated the Hanzo commerce SDK with Backbone.js in 2011 — the router-based checkout flow pattern and MVC applied to storefronts.
Event-Driven Cart State Before React Existed
Building reactive cart state with EventEmitter patterns in the browser in 2010 — the pattern that React would later make mainstream.