OffPeakBreak — 5 countries on the same domain without forking the codebase.
India + UK + USA + Singapore + UAE on offpeakbreak.com. Single backend, multi-tenanted by country. Stripe everywhere except India (Razorpay). GeoIP routing at the CDN edge.
single codebase & SSL cert
(~10 person-weeks engineering)
a config change, not code
The situation
India-only travel platform on offpeakbreak.com. Razorpay-only payments, INR-only prices, English-only copy, no country awareness anywhere in the codebase. Leadership wanted to launch in UK, USA, Singapore, and UAE on the same domain without splintering into five codebases or five separate teams maintaining them.
The diagnosis
The system was structurally single-country: every customer-facing entity hardcoded INR; no Country or CountryConfig table; no locale-aware formatting; no GeoIP routing; payment integration tightly bound to Razorpay's SDK.
The decision
/in/, /uk/, /us/, /sg/, /ae/). Single backend multi-tenanted by country_code. Stripe everywhere except India. i18n at the React layer. CloudFront for free GeoIP routing. Per-country pricing authored locally — no FX-conversion magic that creates trust issues with customers seeing oddly rounded foreign-currency prices.
The architecture, in a sentence each
- URL strategy: path prefix per country, single SSL cert, hreflang tags so Google indexes each country variant separately
- Data:
country_code CHAR(2)on every customer-facing table; backfilled all existing data as'IN'; JPA aspect injectsWHERE country_code = :ctxautomatically — no rewriting 50+ repository methods - Configuration:
Country+CountryConfigtables (currency, locale, payment provider, tax rate, contact info, legal entity); adding country #6 = INSERT into two tables - Payments:
PaymentProviderinterface + router; Razorpay for IN, Stripe for everyone else, future providers plug in cleanly - Routing: CloudFront viewer-country header → nginx prefix-redirect → React reads URL prefix and loads the right CountryConfig; manual switcher always available
- Identity segregation: user-PII flagged in the schema; paves the way for in-country data residency (UK / EU GDPR) without code changes if compliance ever requires it
- Scale: identified 3–5 most-used services and ran them separately behind a load balancer for horizontal scale during peak traffic; rest stays as a tight monolith
The delivery (3 phases)
- Phase 1 (3 weeks): schema additions, country_code backfill (= 'IN'), JPA Country aspect, dormant country switcher, i18n library wired. India site behaves identically to before — zero functional regression.
- Phase 2 (3 weeks): UK launch — Stripe integration, payment router, UK content authored, hreflang tags, GeoIP at edge, beta cohort.
- Phase 3 (4 weeks): US, SG, AE in parallel — config-only additions, per-country legal/contact/T&C copy, per-country Google Search Console properties.
The outcome
Adding a 6th country = config change, not code.
Per-country GSC properties tracking SEO independently.
Per-country data residency now achievable without downtime if compliance ever requires it.
What I'd do differently
Started Stripe KYC at week 1 in parallel with engineering — but the UK launch still slipped one week because of paperwork delays. Always start payment-provider paperwork before any code is written. The lesson: compliance KYC is a long-pole task that lives outside engineering's calendar — schedule it like a hardware order.
Planning a multi-country or multi-tenant rollout?