An order is created in a “pending” state before Razorpay is involved, confirmed once by the client-side verification call, and confirmed again by an asynchronous, signature-verified webhook—so a dropped connection on the customer's side doesn't leave payment and order state disagreeing.
Trade-off: two confirmation paths to keep in sync, not one.← India Hidden GemsEngineering deep-dive
This is what sits behind a travel platform
that looks like a simple website.
How the platform is built, and run—continuing the story from the case study: how the code is shaped, how it ships, and where the trade-offs are.
Two-app monorepo: Next.js frontend, one Go API
MongoDB, one collection per domain
Terraform + GitHub Actions, keyless via WIF
Identical staging and production on Google Cloud
System shape 01
One API owns every business rule.
nginx routes each request at the edge: everything under “/api” goes to the Go API with that prefix stripped, everything else renders through Next.js. Server-rendered pages call the API directly over the internal network for SSR; the browser calls it again through nginx for anything client-side—cart actions, auth, admin work. Either way it's the same API, so there's exactly one place business logic can live.
Module map 02
Grouped by what changes together
Catalog (experiences, trips, handicrafts, boutique stays), editorial (blogs, comments, engagement, testimonials), and commerce (cart, orders, payment, coupons, invoicing) are separate concerns that occasionally talk to each other—an order triggers a notification, a comment triggers engagement scoring—through small interfaces rather than shared internals.
The five-file pattern
Every domain package is shaped the same way: a model with its schema as struct tags, a repository interface, a MongoDB implementation, HTTP controllers, and a route registrar. A new feature is a new package built to that shape, not a redesign of an existing one—so the cost of adding one stays roughly constant as the catalog grows.
Delivery pipeline 03
Staging and production are the same stack, twice.
Each environment is a separate Google Cloud project running an identical Terraform-defined stack: an HTTPS load balancer in front of a regional managed instance group, built from a golden VM image running nginx, the frontend, and the API in Docker Compose.
A push to the main branch triggers GitHub Actions, which authenticates to Google Cloud over Workload Identity Federation—no service-account key ever leaves GCP—builds and pushes both container images, applies Terraform against the compute module with the new image tag, and lets the instance group roll the change out with zero-downtime surge replacement. A health-check smoke test against the load balancer closes the loop; production deploys wait on manual approval.
The credential design behind this, in full ↗Infrastructure & security boundary 04
No public attack surface
Firewall rules deny all ingress by default; the only exceptions are the load balancer's health check and an IAP tunnel scoped to a named identity. There is no public SSH port to scan, brute-force, or forget to close.
Watched, not just deployed
CPU, memory, load-balancer 5xx rate, uptime, and application error-rate each have an alert policy wired to email—so a bad deploy or a resource leak surfaces as a notification, not as a customer complaint.
Reliability under real payments 05
Razorpay may redeliver the same webhook more than once. Handlers check the order's current status before mutating it, so re-processing a payment that's already marked “paid” is a no-op rather than a double-counted coupon or a duplicate confirmation email.
Trade-off: correctness depends on every handler checking status first.The full checkout sequence—create order, charge, increment coupon usage, send confirmation, generate the invoice—isn't wrapped in a database transaction. A crash mid-sequence is recoverable (the webhook redelivers, state can be reconciled by hand) but not automatic.
Trade-off: simpler code today, in exchange for a manual step in a rare failure case.Tech stack 06
Backend
Go, gorilla/mux, MongoDB, Firebase Admin SDK for token verification, a hand-rolled Razorpay client, zap for structured logging, and GCP Secret Manager for configuration in every deployed environment.
Frontend
Next.js App Router on React, TypeScript throughout, Tailwind with CSS-variable theme tokens, Zustand for client state, and the Firebase JS SDK for sign-in. Catalog and editorial pages render server-side for search discoverability; cart, checkout, and admin are client-rendered.
Deliberately scoped for now 07
Flat packages, not hexagonal layering.
One generic Mongo repository, not typed per domain.
No message queue—notifications run in the request path.
Chat state lives in memory until it needs Redis.
No cache layer, added only against a measured hot path.
Want the system behind your product? 08
Let's build it to
actually run.
Architecture, delivery pipelines, and the trade-offs that come with running real payments in production—happy to talk through what your system needs.
Start a conversation ↗