Good Bowls: Building a Production-Grade E-Commerce Platform
Engineering a scalable food ordering system with three-tier architecture, secure payment processing, and modern state management.
The Problem Space
Building an e-commerce platform for a salad bowl restaurant requires solving several interconnected challenges: real-time cart management, secure payment processing, dynamic pricing logic for custom orders, and user authentication, all while maintaining separation of concerns.
The core question: how do you architect a system where the frontend handles optimistic UI updates, the backend enforces pricing integrity, and the database maintains order history accuracy?
Three-Tier Architecture
Presentation Layer
React + Redux for UI rendering and client-side state with localStorage persistence
Business Logic Layer
Express + Node.js with stateless JWT auth for horizontal scaling
Data Layer
MongoDB Atlas with document-oriented storage for natural order modeling
Why Not a Monolith?
Separating client and server wasn't just about following best practices. It solved real deployment constraints. Vercel offers instant CDN-backed static hosting for the React build, while Render provides containerized Node.js hosting.
Benefits
- • Specialized platform advantages
- • Independent scaling per tier
- • Optimal caching strategies
Tradeoffs
- • Network latency between tiers
- • CORS configuration complexity
- • Deployment orchestration
In development, I proxy API requests from port 3000 to 8080 via the client's package.json. In production, explicit CORS headers whitelist the Vercel origin.
State Management Strategy
The cart presented an interesting state management problem: it needs to be reactive (updates reflect immediately), persistent (survives refreshes), and eventually consistent with the backend.
Redux as Single Source of Truth
All cart operations dispatch actions that update a normalized state tree. Components subscribe to derived selectors.
LocalStorage as Persistence
A Redux middleware watches for mutations and syncs to localStorage. On app init, we hydrate from storage.
Backend as Authority on Checkout
The frontend sends the cart to the backend, which validates prices against the database before creating the order.
The Custom Bowl Builder Challenge
Custom bowls introduced complexity: users select a base, multiple toppings, proteins, cheeses, and dressings, each with individual prices. The builder needs to show a running total as users add/remove ingredients.
I solved this with a component-local state machine that tracks selections, then calculates price on-the-fly. When the user clicks "Add to Cart," it dispatches to Redux with the complete specification.
Why not Redux? The builder is ephemeral. Selections only matter while the modal is open. Lifting this to Redux would pollute the global store with transient data.
Authentication & Authorization
The authentication flow uses JWTs with a design choice: the payload includes both a user ID and an admin role flag. This eliminates database lookups on every request just to check permissions.
Verification Middleware
Extracts token from x-auth-token header, verifies signature, attaches decoded payload to req.user
Password Security
Passwords are hashed with bcrypt using async operations (`await bcrypt.hash()`) to avoid blocking Node.js's event loop. Salt rounds are configurable via environment variables.
Tradeoff: JWTs can't be revoked before expiry. For a restaurant app, this is acceptable. The blast radius is limited to order history visibility.
Payment Integration
Payment processing follows Stripe's recommended pattern: the client collects card details using Stripe Elements (PCI-compliant inputs), the backend creates a PaymentIntent, and the client confirms payment.
Key Insight: Never send the dollar amount from the client. The backend always calculates totals from database prices to prevent client-side manipulation.
Database Design Patterns
Price as Array
Bowl prices stored as [Small, Medium, Large] arrays. Keeps size pricing denormalized for fast reads with no joins on menu fetch.
Order Snapshots
Orders embed full details rather than referencing bowls by ID. This creates a snapshot at order time. If menu prices change later, historical orders remain accurate.
Repository Pattern
Database access abstracted behind repositories. Controllers call createOrder(data) rather than invoking Mongoose directly. This keeps business logic testable.
Deployment & The Cold Start Problem
Free-tier Render hosting spins down the container after 15 minutes of inactivity. The first request after downtime takes up to 50 seconds while the container restarts.
Cron Ping
Keep service warm with scheduled pings every 10 minutes
Paid Tier
Upgrade to persistent containers without cold starts
Serverless
Move to functions with better cold start characteristics
For a portfolio project, the cold start is an acceptable UX tradeoff. The frontend loads instantly from Vercel's CDN, and a loading indicator manages user expectations.
Technical Depth: The Interesting Bits
Race Conditions in Cart Updates
When multiple tabs are open, localStorage writes from one tab don't automatically sync to Redux in others. I handle this with a storage event listener that watches for external changes and dispatches sync actions.
Dynamic Pricing Validation
Custom bowl price = base + toppings + proteins + cheeses + dressing + sauce. The backend recomputes and validates by fetching each ingredient from the database.
If the client-reported total differs by more than $0.01, the request is rejected. This protects against code manipulation, mid-cart price changes, and floating-point rounding errors.
Middleware Composition
Each middleware is focused and reusable: auth verifies JWT, isAdmin checks role, validateBowl runs Joi schema validation.
Lessons Learned
What Worked Well
- Redux Toolkit eliminated boilerplate
- DevTools invaluable for debugging
- Chakra + Ant Design mix worked
- Layered architecture simplified swaps
What I'd Change
- Add email verification with Gmail SMTP
- Validate cart on every page load
- Add order status tracking state machine
- Implement webhook for payment confirmation
"Every architectural decision is a tradeoff."
Building this system taught me that good architecture isn't about perfection. It's about making intentional choices you can defend.