Back to System Design
System Design
medium
mid

How do you expose an SDK API such as open() and close() without polluting the host page's global namespace?

Wrap everything in an IIFE/module so internals stay private via closure; expose exactly ONE namespaced, frozen global (window.Razorpay) whose methods are the public API. Guard against double-injection, avoid prototype pollution, and treat the global's shape as a stable contract.

4 min read·~8 min to think through

The goal: the merchant's window gets exactly one new property, that property's internals are inaccessible, and the public surface is a deliberate, stable API.

The pattern: closure + a single namespaced global

Everything lives inside an IIFE (or an ES module bundled to one) so all internal variables, helpers, and state are private via closure — nothing leaks to window:

js
(function () {
  // --- all private: closure-scoped, invisible to the host page ---
  let iframe = null;
  let isOpen = false;
  function injectIframe(config) { /* ... */ }
  function teardown() { /* ... */ }

  // --- the ONE public global ---
  const Razorpay = {
    open(config) {
      if (isOpen) return;          // guard
      iframe = injectIframe(config);
      isOpen = true;
    },
    close() {
      if (!isOpen) return;
      teardown();
      isOpen = false;
    },
  };

  // guard against the script being included twice
  if (!window.Razorpay) {
    window.Razorpay = Object.freeze(Razorpay);  // freeze: merchant can't tamper
  }
})();

The principles

  1. One global, namespaced. Not window.open/window.close (collision with built-ins!) — one object, window.Razorpay, and open/close are methods on it. The whole API hangs off a single property.
  1. Internals private via closure. iframe, isOpen, every helper — all closure-scoped inside the IIFE. The merchant's code literally cannot reach them. No internal state on window.
  1. Freeze the public object. Object.freeze so the merchant (or a malicious script on their page) can't overwrite open/close or add properties. The contract is immutable.
  1. Guard against double-injection. If two <script> tags load it, don't clobber an existing instance or run setup twice — check if (!window.Razorpay).
  1. Don't touch shared prototypes. Never modify Object.prototype, Array.prototype, etc. — that's the worst kind of pollution and would break the host page.
  1. Treat the global's shape as a public contract. Once merchants depend on Razorpay.open(), its signature and behavior must stay backwards-compatible — version behavior internally, not the API shape.

Why it matters

You're a guest. Polluting window risks name collisions (you overwrite something the merchant uses, or vice versa), security leaks (internal state readable/tamperable), and debugging nightmares for the merchant. One frozen, namespaced global is the minimal, safe footprint.

The framing

"I wrap the whole thing in an IIFE so every internal — the iframe reference, open state, helpers — is private via closure and never touches window. Then I expose exactly one namespaced global, window.Razorpay, with open/close as methods on it — never bare globals that could collide with built-ins. I Object.freeze it so the host can't tamper with the API, guard against double-injection, and never touch shared prototypes. The merchant's window gains one immutable property; everything else is sealed in the closure. And that one global's shape becomes a stable public contract."

Follow-up questions

  • Why expose open/close as methods on one object instead of two globals?
  • How does the closure keep internal state private?
  • Why freeze the public object?
  • How do you handle the script being included twice?

Common mistakes

  • Adding multiple globals, or names that collide with built-ins (open, close, config).
  • Leaking internal state onto window.
  • Not freezing the API, letting the host overwrite methods.
  • Modifying shared prototypes.
  • No guard against double-injection.

Performance considerations

  • Negligible runtime cost — this is about footprint and safety, not speed. Keeping the public surface minimal also keeps the loader script small.

Edge cases

  • Two script tags loading the widget.
  • Merchant already has a global with the same name.
  • Malicious host script trying to overwrite Razorpay.open.
  • Merchant code reading/mutating what they think is internal state.

Real-world examples

  • Razorpay, Stripe, Intercom, Google Analytics — all expose a single namespaced global from an IIFE.
  • Any third-party embed script that must coexist with arbitrary host pages.

Senior engineer discussion

Seniors describe the IIFE-closure-for-privacy + single-frozen-namespaced-global pattern, avoid built-in name collisions, guard double-injection, never touch shared prototypes, and treat the global's shape as a versioned public contract.

Related questions