Multi-Currency: Architecture for Borderless Commerce
How to serve 150 currencies without breaking your CDN cache. A deep guide to Internationalization (i18n), Floating Point math, and Edge Routing.
Selling globally is no longer an “Enterprise” feature. A kid in a garage in Brooklyn can sell T-shirts to users in Tokyo. But showing “Price: $25.00” to a Japanese user is a friction point. They have to do mental math ($25 * 150 = ¥3750?). The moment you introduce Multi-Currency, you introduce one of the hardest problems in web engineering: Context-Aware Caching.
At Maison Code Paris, we build Global-First architectures. We don’t view currency as a UI toggle; we view it as a fundamental dimension of the application state, arguably as important as the URL itself.
Why Maison Code Discusses This
At Maison Code Paris, we act as the architectural conscience for our clients. We often inherit “modern” stacks that were built without a foundational understanding of scale. We see simple APIs that take 4 seconds to respond because of N+1 query problems, and “Microservices” that cost $5,000/month in idle cloud fees.
We discuss this topic because it represents a critical pivot point in engineering maturity. Implementing this correctly differentiates a fragile MVP from a resilient, enterprise-grade platform that can handle Black Friday traffic without breaking a sweat.
The Caching Paradox
In a standard web application, your CDN (Cloudflare/Fastly) caches the HTML of the homepage.
- User A (USA) visits
example.com. - Server renders HTML:
Price: $100. - CDN caches this HTML.
- User B (France) visits
example.com1 minute later. - CDN serves cached HTML:
Price: $100.
This is a disaster. User B sees dollars. They add to cart, and maybe the checkout switches to Euros later, or maybe they just leave because they don’t want to pay conversion fees. To fix this, we need the server to know who is asking. But if the server knows, the CDN can’t just serve a static file.
Strategy 1: The Subfolder (The Gold Standard)
The most robust technical solution is to make the currency explicit in the URL.
example.com/en-us-> Serves USD.example.com/fr-fr-> Serves EUR.example.com/jp-jp-> Serves JPY.
Why this wins:
- Cache Efficiency:
/fr-fris a unique URL. We can cache it aggressively at the edge. Every French user gets a cache hit. - SEO: Googlebot knows exactly what currency matches what region. You can set
hreflangtags easily. - Clarity: The user knows where they are.
Implementation in Remix/Hydrogen:
// app/routes/($locale)._index.jsx
export async function loader({ request, context, params }) {
const { locale } = params; // "fr-fr"
const currency = getCurrencyFromLocale(locale); // "EUR"
// Pass to Shopify API
const products = await context.storefront.query(PRODUCTS_QUERY, {
variables: { country: getCountryCode(currency) }
});
return json({ products });
}
Strategy 2: The Vary Header (The Dynamic Solution)
If you must keep the URL clean (example.com for everyone), you have to instruct the CDN to cache multiple versions of the same URL.
We use the Vary header.
Vary: CF-IPCountry
This tells Cloudflare: “Please store a separate copy of this page for every unique Country Code.”
- User (US) -> CDN looks for “Homepage + US”. Miss. Fetches from Server. Caches.
- User (FR) -> CDN looks for “Homepage + FR”. Miss. Fetches from Server. Caches.
- User (US) -> CDN looks for “Homepage + US”. Hit.
The Downside: Cache Fragmentation. If you support 200 countries, you effectively divide your cache hit ratio by 200. Your origin server takes more load.
Strategy 3: Client-Side Painting (The Hybrid)
For high-performance, strictly cached pages, we sometimes cache the Generic Skeleton.
The HTML returned contains no price, or a placeholder.
Then, useEffect on the client fetches the local price.
function Price({ baseAmount }) {
const { currency, rate } = useCurrencyContext();
if (!rate) return <Skeleton />;
return <span>{formatMoney(baseAmount * rate, currency)}</span>;
}
Warning: This causes Layout Shift (CLS) and a “Flash of Unpriced Content”. It feels cheap. Use only as a last resort.
The Shopify Markets Architecture
In the Shopify ecosystem, we utilize the Contextual Storefront API. You do not calculate exchange rates yourself. You let Shopify’s backend handle it. Why? Because Shopify allows merchants to set Fixed Exchange Rates or Price Adjustments per market. Perhaps the shirt is $20 in the US, but $25 in Europe (to cover VAT and Shipping), not just a direct $20 * 0.92 conversion.
# The Magic Directive: @inContext
query GetProduct($handle: String!, $country: CountryCode!)
@inContext(country: $country) {
product(handle: $handle) {
priceRange {
minVariantPrice {
amount
currencyCode # Automatically returns the local currency
}
}
}
}
By passing the country code, we offload the complexity of pricing logic to the platform engine.
Money Math: Floating Point Nightmares
Never use standard JavaScript numbers for money.
0.1 + 0.2 === 0.30000000000000004
If you are building a custom cart, you will lose a cent on every 10th order. Over a million orders, that’s $10,000 lost to floating point errors. Rule: Store money in Cents (Integers).
- $10.00 ->
1000 - $19.99 ->
1999
Use libraries like Dinero.js or Currency.js for all client-side math.
import Dinero from 'dinero.js';
const price = Dinero({ amount: 5000, currency: 'EUR' }); // 50.00 EUR
const tax = price.percentage(20); // VAT
const total = price.add(tax);
Psychological Pricing and Rounding
Algorithmic conversion is ugly.
- $100.00 * 0.92 = $92.00.
- But $92.00 feels “random”.
- The psychological price should probably be $95.00 or $89.00.
We implement Rounding Strategies at the application layer (if not handled by the backend).
function roundToCharmPrice(amount: number): number {
// 92.34 -> 95.00
// 98.10 -> 99.00
const heavyPart = Math.floor(amount);
const lastDigit = heavyPart % 10;
if (lastDigit < 5) return heavyPart - lastDigit + 5; // Round to 5
return heavyPart - lastDigit + 9; // Round to 9
}
Note: Usually, we advise clients to set these explicitly in Shopify Markets, but having a fallback logic is useful for dynamic bundling.
SEO: The Metadata of Money
Google fails to index your prices if they are dynamic (JS-injected). If you use Strategy 1 (Subfolders), standard JSON-LD structured data works perfect.
<script type="application/ld+json">
{
"@context": "https://schema.org/",
"@type": "Product",
"name": "Luxury Watch",
"offers": {
"@type": "Offer",
"priceCurrency": "EUR",
"price": "5000.00"
}
}
</script>
Ensure this block matches the visible content. If the JSON-LD says EUR and the visible text says USD (due to client-side switch), Google Merchant Center will ban your account for “Price Mismatch”.
13. The Refund Currency Risk
User buys for $100 ($110). Next week, dollar crashes. $100 is now $120. User requests refund. Do you refund $100 (Customer is happy, You lose $10)? Or do you refund $110 converted to Euros ($92) (Customer is furious)? Legal answer: Refund the original currency amount ($100). Financial answer: You take the FX risk. We build “Refund Reserves” logic in the ledger to account for this fluctuation as “COGS - FX Loss”.
14. Hedging and Treasury Management
If you sell $10M in Euros, you are holding a lot of Euros. If the Euro crashes, you lose money before you can pay your US factory. This is where Automated Hedging comes in. We integrate with Wise Business API or Airwallex. When balance > $10,000, auto-convert to USD. This “Micro-Hedging” strategy reduces exposure to macro-economic events.
15. Payment Gateway Rejections (Currency Mismatch)
Stripe allows you to charge in any currency.
But some local banks (e.g., in Brazil or India) reject “Foreign Currency” transactions even if the card supports it.
The Fix: Smart Retries.
If a charge fails with do_not_honor, retry immediately in the card’s issuing currency (if detectable via BIN).
“You tried to charge $100. Failed. Retry charging $92. Success.”
This improves authorization rates by 4%.
16. Cash on Delivery (COD) and Currency
In the Middle East (GCC), 60% of orders are COD. The courier collects Cash. The courier does not accept USD. They only accept AED/SAR. If you display USD on the checkout, the courier will arrive and ask for a different amount (Exchange Rate of the day). Rule: If COD is selected, you MUST lock the price in local currency at checkout and print it on the physical manifest. Discrepancy here leads to “Refused on Delivery”.
17. Conclusion
Multi-currency is not a “Plug-in”. It is a fundamental architectural decision that touches Routing, Caching, Database Design, and SEO. At Maison Code, we believe true Borderless Commerce is invisible. The user lands, sees their currency, pays with their local method, and receives the goods. The complexity is our burden, not theirs.
Going Global?
If you are struggling with Vary headers or broken caches.