What eight years on storefronts actually teaches you
I've spent the last stretch of my career on the frontend of e-commerce: the storefront of jamtangan.com, catalog and ordering flows at voila.id, and the internal systems around them. Long-term maintenance teaches different lessons than greenfield builds. You stop optimizing for the demo and start optimizing for the year after the launch — the year where every change has to land without breaking a checkout someone is in the middle of.
What follows are principles, not war stories. I'm deliberately not telling the incidents. The lessons are the part worth keeping.
1. State needs owners, and the owners should be few
Early in a project, state scatters. A loading flag lives in one component, the cart lives in another, a filter value lives in a third. Six months later nobody can answer a simple question: where does the cart live?
On both storefronts I settled on domain-sliced stores — Zustand in practice — with one rule: a piece of client state has exactly one home.
export const useCartStore = create<CartState>()((set) => ({
items: [],
addItem: (item) =>
set((state) => ({ items: [...state.items, item] })),
clear: () => set({ items: [] }),
}))
That's the whole pattern. It isn't clever. Its value is that a new engineer can answer "where is the cart state" in one grep. On GraphQL projects I extend the rule: the server cache owns what the server knows, the client store owns what it doesn't, and nothing lives in both.
2. API contracts change; defend the boundary, not the components
In long-running commerce systems, the API is never finished. A field gets deprecated. A response wraps from bare array to paginated envelope. A price becomes an object with currency.
The defense is a typed boundary. I parse and validate at the edge — a transformer, a codegen'd client, a schema validator — and components consume only the normalized shape. The anti-pattern I've seen burn teams: component-local response typing that mirrors whatever the API happened to return that week. When the API changes, the breakage should land in one file per resource, not in forty components.
3. Catalog pages are performance work in disguise
Product listing pages carry the heaviest load in any storefront: dozens of images, facets, prices, badges, analytics. The lesson isn't a single trick — it's a budget mentality.
Serve the grid from the server, cache it, and make the first paint not depend on client JavaScript. Lazy-load images with correct dimensions so nothing jumps. Defer everything that isn't the catalog itself — recommendation widgets, review summaries, chat launchers — until after the content is interactive. And measure on a mid-range phone over a throttled connection, because that's where your buyers are, not on your laptop over fiber.
4. Treat checkout as a protected zone
Checkout is where the money is. The lesson: small changes, reversible releases, and a bias toward boring code in that flow. Clever state machines and speculative abstractions have a place — not in the path between "add to cart" and "payment confirmed."
I also stopped assuming a green test suite means a safe checkout. The flow involves third parties, network flakiness, and stock that changes between pages. Manual verification of the full path before release isn't ceremony; it's the last line of defense.
5. Loading, empty, and error states are the design
A product grid that renders perfectly with data and shows nothing during a slow request is not finished work. On both storefronts I've treated these states as part of the feature definition: what does the catalog look like while facets load? What does an empty search show? What happens when the price service times out mid-page?
This is also where frontend-backend-design collaboration either works or doesn't. The fastest way I've found to have the conversation: walk the flow together and name every state out loud before writing code. Designers catch states engineers would paper over with a spinner. Backend engineers tell you which failures are retryable. It takes an hour and saves a week of "wait, what happens if…".
6. Consistency compounds; cleverness doesn't
Two storefronts, years of weekly releases, several engineers rotating through. The thing that kept velocity up wasn't a brilliant abstraction — it was sameness. The same table pattern across admin modules. The same form conventions. The same API error handling in every page. New work could copy the nearest neighbor and be 80% correct before review.
Clever, bespoke solutions per page feel productive the week you write them and expensive every week after. For a team that ships continuously, consistency is the performance optimization — of the human kind.
The reflective version
If I compress eight years into one sentence: e-commerce frontend is not about frameworks, it's about trust — the buyer's trust that the page will load, the price will be right, and the button they clicked did what they think it did. State ownership, honest contracts, performance budgets, protected checkout, complete states, and boring consistency are all just different expressions of that.
The frameworks changed underneath me more than once. These didn't.