An engineering journal from a recent hardening pass on a live e-commerce platform — Django backend, web storefront, and a Capacitor-based iOS/Android app, all shipping to real customers at the same time.
Two orders. Same product, same delivery address, placed one minute apart. One came in through the website, one through the mobile app.
The website booked a courier at ฿37. The app booked in-house delivery at ฿25 — for an address in-house couriers have no way of reaching. Nothing crashed. Nothing logged an error. Two customers, two silently different outcomes, and the only reason anyone caught it was a habit of cross-checking real orders against each other instead of trusting that "it works" on either platform alone.
That’s the failure mode that defines multi-platform e-commerce: not the bugs that crash, the ones that quietly disagree. A backend that calculates prices, discounts, and shipping; a web storefront that has to render all of that correctly; a mobile app that has to reach the same correct answer independently, on a different framework, under different platform rules — and two app stores that will reject your build over a missing metadata file. What follows is a walk-through of one recent pass across all three layers on a live platform, hunting exactly these disagreements. It’s the kind of work we do at Simplico for e-commerce clients — not a demo build, a production system with real checkout traffic while the work was happening.
Backend: where correctness actually lives
The backend decides an order’s shipping cost, courier, and discount — and a wrong answer here is the most expensive kind of bug, because it’s the one that reaches a customer’s total.
Shipping quotes were silently failing for most of the catalog. The platform quotes courier rates by 3D bin-packing the cart’s items into standard box sizes before asking the courier API for a price — until an item is missing a width, height, or length in the catalog, at which point the packer quietly returns zero usable boxes instead of an error. In a spot check, that was true for 485 of 556 product variants. No boxes, no quote, and checkout fell back to in-house delivery with nothing in the logs to explain why. We replaced the packing step with a resilient aggregate-parcel calculation that always produces a usable quote from whatever dimensions exist — a gap in the catalog now degrades gracefully instead of silently rerouting an order.
Payment reconciliation needed a single source of truth. Two payment gateways, one fallback path between them, and post-payment side effects — stock deduction, confirmation emails, courier booking — that needed to fire identically no matter which gateway actually processed the charge. We consolidated them into one shared path, and added a visible payment-gateway indicator to the operations dashboard so support staff can see at a glance which one handled a given order.
Checkout crashes traced back to data, not code. A promotional-campaign lookup used .get() where duplicate rows were structurally possible — harmless until a campaign actually accumulated one, at which point it took checkout down with an exception that looked, from the outside, like a payment failure. Made the lookup deterministic instead of exception-prone.
Web: making the interactive layer actually interactive
The storefront’s checkout page layers a lightweight reactive framework (Alpine.js) over server-rendered Django templates — pragmatic, and with one specific failure mode if you’re not careful: you can build interactive markup as a string, inject it, and it will render perfectly while doing absolutely nothing. That’s what had happened to the shipping-method radio buttons — constructed as raw HTML strings and inserted via an innerHTML-style directive, which never re-parses the interactive bindings inside it. They looked correct in every screenshot and had been non-functional for an unknown stretch of time. Rebuilt as properly compiled template markup.
A subtler version of the same lesson showed up twice more: server-rendered conditionals and client-rendered conditionals don’t compose safely when nested inside each other. Once for an address form that only exists for customers with zero saved addresses, once for a coupon block gated behind having any coupons to select — both left the client-side framework holding a reference to something the server hadn’t actually rendered. Restructured so the server decides whether a block exists, and the client only ever decides how to display it.
Mobile: two platforms, two rulebooks, one product
Mobile carries an extra tax a web team doesn’t pay: the platform vendors are also your gatekeepers, and their rules change under you.
App Store rejection, ITMS-91061. Apple now requires a privacy manifest declaring why an app uses certain "commonly abused" APIs — and five bundled SDKs in the dependency tree didn’t ship one. The obvious fix doesn’t survive a real build pipeline: dependency installation regenerates the folder you’d naively drop a file into, every single time. We built a durable solution instead — manifests tracked in source control, injected and registered automatically on every install, verified idempotent across repeated builds.
Google Play policy and an Android 16 deadline, in the same week. The app’s camera dependency declared broad gallery-access permissions unconditionally — a pattern Play’s reviewers now actively flag. We migrated photo and video selection to the platform’s native system pickers, which need no runtime permission at all, and brought the target SDK up to Android 16 ahead of Play’s enforcement date. Both verified against real device logs — an early assumption about the old plugin’s permission behavior turned out to be wrong, and only a live device trace caught it.
Sign-in worked on Android and silently failed on iOS. One missing configuration line — the wrong audience on a requested identity token — invisible until you inspect what the token actually contains.
And the order we opened with. The mobile checkout had logic the web checkout didn’t: pick whichever of two shipping options is cheaper, everywhere — including for addresses the cheaper option can’t reach. Easy to miss building platforms in isolation; easy to catch testing them against each other with real order data, which is exactly how it surfaced.
Changing the API’s domain no longer requires an app store review. We added a remote-configuration step to app startup: on every launch, the app checks a lightweight config endpoint for its API domain, applies it immediately, falls back safely if the check fails. What used to be a rebuild-and-resubmit — days of lead time, minimum — is now a one-line server-side change that takes effect the next time a customer opens the app.
Performance: the parts nobody sees
Correctness bugs get reported — a customer sees a wrong price and says something. Slow pages don’t get reported at all; the customer just leaves, and nothing in the logs says why. So this thread of work started as a full-app audit rather than a bug hunt: three parallel passes across database queries, caching, and background jobs, then fixes prioritized by measured impact instead of guesswork.
One customer’s order history took 9.7 seconds and ran 1,746 database queries to load. Two N+1 patterns stacked on top of each other: order items and their products weren’t prefetched at all, and — the sharper find — the ones that were prefetched were being silently bypassed by an innocent .order_by() chained after them, which skips Django’s prefetch cache and re-hits the database anyway. Fixed both layers: 3.5 seconds, 406 queries — a 77% cut in database load.
A live flash sale made one specific page catastrophically slow. Every product on a category listing was independently re-deriving "which promotional campaigns are active right now" — identical work, repeated once per product, on every page load. Cached the answer once per request instead. One category page went from 10.2 seconds to 0.85 seconds.
A menu rendered on every single page turned out to be the second-most expensive thing on the server. Profiling traced roughly a quarter of one page’s total render time to recursively rebuilding a 209-category dropdown from scratch, on every request, even though the category tree barely ever changes. It lives in the base template every page extends, so one cached fragment fixed it once — and the win applies site-wide, not just to the page that got profiled.
And the raw, unresized upload images from earlier in this post — the 3.9MB slideshow banner that now ships at 14.9KB — are the same discipline applied to a different layer: an image is just another query that’s expensive if nothing caches or resizes it.
Not every proposed fix earns its keep, and we’d rather say so than round up. One caching change was implemented, measured, and moved nothing on its target page — the value it cached was never read more than once per request, so there was nothing to deduplicate. Kept anyway, because measuring it surfaced a real correctness bug along the way. The performance number for that one is honestly zero, and it’s logged that way.
The throughline
None of this was exotic. A missing config line, a plugin declaring more than it needs, an innerHTML shortcut, a comparison that only made sense in half the app’s coverage area. What made each one worth finding is that it lived at a seam — backend and frontend, web and mobile, what a platform vendor requires this year against what a codebase assumed last year. Small individually. Collectively, the difference between a storefront that mostly works and one that reliably does — the difference between two customers getting the same order at two different prices, and not.
That seam-work — backend, web, and mobile, kept honest against each other, on a real production system — is what we do at Simplico. If you’re running an e-commerce platform and want a team that treats "it works on one platform" as a bug report rather than a milestone, that’s exactly what our e-commerce engineering service is built for.
Latest Posts
- EUDR Is Coming for Thai Rubber and Palm Oil — Is Your Depot’s Paperwork Ready? July 26, 2026
- Your SOC Watches Your Employees. It Doesn’t Watch Your Vendors. July 23, 2026
- Build vs Buy: Should You Deploy Your Local LLM In-House, or Hire a Partner? July 22, 2026
- Why Your RAG Pipeline Keeps Leaking Data It Shouldn’t: Access Control at the Retrieval Layer July 18, 2026
- Why Factories Fear ERP Failure — And the Sync Layer That Fixes It July 15, 2026
- Why Semiconductor and Electronics Manufacturers in Southeast Asia Are Outgrowing Traditional MES July 15, 2026
