DIKA
GET IN TOUCH

Atlas Checkout

Last updated Jul 11, 20262026

Atlas Checkout is the payment flow for a mid-sized commerce platform: cart review, address, shipping method, payment, confirmation. I rebuilt it over one quarter while it kept taking live orders.

Tip

The core change wasn't the UI. It was moving checkout state out of the browser and making the server the only thing that decides what a cart costs.

Problem

Checkout state lived in client-side context: cart contents, applied discounts, shipping quotes, and the computed total. Three consequences:

  • Prices could disagree. The browser computed a total for display; the payment intent recomputed it on the server. Any drift produced either a failed payment or a wrong charge.
  • Nothing was testable in isolation. Pricing rules were spread across React components, so verifying "stacked discounts never go below zero" meant driving a browser.
  • Recovery was impossible. A dropped connection at the payment step lost the entire session; users restarted from an empty cart.

Definition of done: one authoritative total, a pricing module testable without a browser, and a resumable session.

Architecture

Checkout became a server-owned state machine. The client renders the current step and submits transitions; it never derives money.

Browser                    Next.js server                 Postgres
   │                            │                            │
   │  POST /checkout/:id/step   │                            │
   ├───────────────────────────►│  load session ────────────►│
   │                            │  transition(state, event)  │
   │                            │  price(cart) ── pure fn    │
   │                            │  persist ─────────────────►│
   │  ◄─── rendered step ───────┤                            │
   │                            │                            │
   │                       Stripe intent (server-side only)   │
  • checkout_sessions table holds the state machine's current node plus the full event log. A session can be replayed, which is what makes recovery work.
  • price() is pure. It takes a cart, a discount list, and a shipping quote, and returns line items and a total. No I/O, no framework imports.
  • Transitions are validated server-side. Skipping to payment without a valid address is rejected by the state machine, not by a disabled button.
  • Payment intents are created from the persisted total so the number the user agreed to is the number that gets charged, by construction.

Pricing as a pure function

export function price(cart: Cart, discounts: Discount[], shipping: Quote) {
  const lines = cart.items.map(toLine);
  const subtotal = sum(lines.map((l) => l.amount));
  // Discounts are folded in a fixed order so the result is deterministic
  // regardless of the order the user applied them in.
  const reduction = orderDiscounts(discounts).reduce(
    (acc, d) => acc + d.apply(subtotal - acc),
    0,
  );
  return { lines, subtotal, reduction, shipping: shipping.amount,
           total: Math.max(0, subtotal - reduction) + shipping.amount };
}

That signature made the invariant testable: the total never drops below zero, and discount ordering never changes the result.

Stack

Next.js App Router

Server actions for transitions, streamed steps, no client-side price computation.

Postgres

Session state plus an append-only event log per checkout.

Stripe

Payment intents created server-side from the persisted total.

Zod + Vitest

Boundary validation and property tests over the pricing module.

Results

MetricBeforeAfter
Payment failures from total mismatch~1.4% of attempts0
Median time to complete checkout96s71s
Abandoned sessions recoverednone possible18% resumed
Pricing test suite runtime4m (browser)1.2s (node)

The mismatch number going to zero isn't optimization, it's the class of bug being structurally removed — there's only one place a total can come from now.

What I'd do differently

I modeled the state machine by hand with a transition() switch. It was clear at five states and awkward at nine, once partial refunds and gift cards arrived. A declarative machine definition would have made the illegal transitions visible as data rather than as missing switch cases.

I'd also persist shipping quotes with an explicit expiry from day one. We treated them as fresh forever, then discovered stale quotes during a carrier price change — a fix that would have cost one column at the start.