# Frontend System Design: Build a Stripe-Style Checkout Component

Source: https://www.techinterview.org/post/3233475026/frontend-system-design-stripe-checkout/
Updated: 2026-06-30 · techinterview.org

"Build a checkout form like Stripe" is a beautifully focused frontend system design question. It touches PCI compliance, iframe sandboxing, real-time validation, error handling, accessibility, and the nasty edge cases of credit card input (formatting, masking, BIN detection).

## Functional requirements

- Card number, expiry, CVC, ZIP fields

- Real-time validation as user types

- Card type detection from BIN (Visa, Mastercard, Amex)

- Error messages on submit and per-field

- Submit button disabled until form is valid

- Apple Pay / Google Pay buttons where supported

## The PCI scope problem

If raw card numbers touch your servers or your JavaScript, you are in PCI-DSS Level 1 scope — onerous compliance. Stripe Elements solves this by serving the card input fields from a Stripe-hosted iframe. Your page hosts the iframe; it cannot read the contents.

Architecture:

- Your page loads `stripe.js`

- You instantiate `cardElement = elements.create("card")` and mount it

- Stripe injects an iframe — the actual card input is in the iframe DOM

- On submit, you call `stripe.createToken(cardElement)`

- Stripe.js communicates with the iframe via postMessage, returns a token

- You send the token to your server, which charges through Stripe API

Result: your code never sees the card number. PCI scope reduces to SAQ-A.

## Real-time validation

Standard Luhn check for card number. Detect card type from BIN (first 6–8 digits) — Visa starts with 4, Mastercard 5, Amex 34/37, etc.

Format on the fly: "4242 4242 4242 4242" with spaces every 4 digits. Amex uses "4-6-5" format. Update masking based on detected card type.

## Validation timing

- **On blur:** validate after the user leaves the field

- **On submit:** validate the whole form

- **Live during typing:** only show errors after the field has been touched once

Showing errors on every keystroke is hostile. Don't do it.

## Error handling

Possible failure modes:

- Validation errors — handled inline

- Network errors — retryable, show error banner

- Card declined — server-returned, show specific message

- 3DS challenge required — Stripe handles via redirect or modal

- Fraud block — generic "transaction failed" message

## Apple Pay / Google Pay

Use Payment Request API (browser standard) or platform-specific buttons. The Payment Request flow:

- Detect support (browser API exists, payment methods enrolled)

- Show button

- On tap, browser shows native payment sheet

- User authorizes

- Browser returns payment token to your JS

- Send to server, charge via Stripe

## Accessibility

- Each field has `<label>` properly associated

- aria-invalid on error states

- aria-describedby pointing to error messages

- Focus management on submit failure (jump to first invalid field)

- Submit button announces "submitting" via aria-live

## Submit flow

- Disable submit button on tap

- Show loading spinner

- Tokenize via Stripe.js

- POST token + idempotency key to your server

- Server returns success/failure

- On success: redirect to receipt page

- On failure: re-enable submit, show error

## Common mistakes

- Showing all errors at once on submit (overwhelming)

- Disabling submit until all fields are valid (frustrating — user can't test the submit handler)

- Allowing double-submit by not disabling the button

- Storing card number in a hidden field (massive PCI violation)

## Frequently Asked Questions

### Why use iframes instead of just a plain form?

PCI compliance. Iframes hosted by Stripe mean your page cannot access card data, dramatically reducing your audit scope.

### How do you style fields inside an iframe?

Stripe Elements accepts CSS-in-JS-style configuration for fonts, colors, padding. The iframe receives the styles via the API.

### Should I store cards on my server?

No — store Stripe customer/payment-method IDs. The actual card data stays at Stripe.
