System design · Payments · Part 1

Payment system basics

What a payment service does, who takes part in a card payment, how a payment moves through states, and how to store money safely.

The scenario

Imagine an ecommerce site. A customer sees a product that costs $100 and clicks Buy now.

Wireless headphones

$100.00

Buy now

Behind that one click, the request has to travel all the way to the customer's bank:

  1. Customer

  2. Website

  3. Backend

  4. Payment provider

  5. Card network

  6. Customer's bank

The path of a payment, from the click to the customer's bank.

What is a payment service

A large application like Amazon is split into many services, each with one job:

  • UserService
  • ProductService
  • CartService
  • OrderService
  • InventoryService
  • PaymentService
  • ShippingService
  • NotificationService

The payment service handles everything related to payments, for example:

  • Create a payment
  • Confirm a payment
  • Track a payment
  • Refund a payment
  • Handle responses from the payment provider
  • Store payment history
  • Prevent duplicate charges

Payment providers

A payment service does not necessarily move money itself. Usually it talks to another company that processes the card, such as Stripe, PayPal or Braintree.

These companies go by several names: payment processor, payment service provider (PSP) or payment gateway. This note uses Stripe as the example.

  • Stripe

  • PayPal

  • Braintree

  1. Customer

  2. Frontend

  3. Payment service

  4. Stripe

  5. Visa / Mastercard

  6. Customer's bank

The same path, with Stripe as the payment provider.

1.Why not talk to the bank directly?

It's possible, but payment processing is extremely complicated. It involves:

  • Banks and card networks
  • Fraud systems
  • PCI requirements
  • 3D Secure
  • Different countries and currencies
  • Refunds and chargebacks
  • Authorization rules and settlements
  • Compliance, and more

A provider takes on that complexity. The application only says “Please charge this customer $100”, and the provider handles the complicated parts under the hood. Companies like OpenAI and Anthropic use Stripe for exactly this reason.

The actors

Before the architecture, it helps to know who takes part. Imagine Nithin buys a laptop for $1,000. Several parties are involved:

ActorRoleExample
CustomerThe person buying the laptopNithin
MerchantThe company selling somethingAmazon
Payment processorHelps process the paymentStripe
Card networkRoutes the transactionVisa, Mastercard
Issuing bankThe customer's bank, which issued the cardChase, Huntington
Acquiring bankThe merchant's bank, involved in accepting payments—

The request travels from the customer to the issuing bank, and the answer travels back the same way:

Request

  1. Customer

  2. Merchant

  3. Payment processor

  4. Card network

  5. Issuing bank

Response

  1. Issuing bank

    Approved / declined

  2. Card network

  3. Payment processor

  4. Merchant

  5. Customer

The bank approves or declines, and the answer retraces the path.

What happens on Pay

Suppose a customer buys headphones for $100, enters their card details and clicks Pay. The frontend might send a request like this:

POST /payments
{
  "orderId": "123",
  "amount": 10000,
  "currency": "usd"
}

Notice the amount is 10000, not 100. Money is stored in the currency's smallest unit, and for USD that's cents: $100 = 10,000 cents. The last section explains why.

1.Create a payment record

The backend receives the request and first creates a payment record:

payment_id: "pay_123"
order_id:   "123"
amount:     10000
currency:   "usd"
status:     CREATED

2.Ask the provider

Then it asks Stripe: “Please process $100 for this customer.” Stripe contacts the rest of the payment network, and eventually the answer comes back as success or declined.

3.Update the status

The payment's status changes depending on the answer:

Success

  1. CREATED

  2. PROCESSING

  3. SUCCEEDED

Declined

  1. CREATED

  2. PROCESSING

  3. FAILED

This idea is called a state machine.

What is a state machine

A state machine is an object that can be in one of a fixed set of states, and moves between those states according to rules. It works much like a value held in React's useState, except only certain changes are allowed.

Think of a food delivery:

  1. Placed

  2. Preparing

  3. Picked up

  4. Delivered

An order can't jump from Placed to Delivered, or go from Delivered back to Preparing. A payment works the same way. In its simplest form:

CREATED

PROCESSING

SUCCEEDED

Approved

FAILED

Declined

A payment moves forward only, and ends in exactly one final state.

Real payment systems usually have more states:

StateMeaning
CREATEDThe payment record exists
REQUIRES_ACTIONThe customer must do something, like 3D Secure
PROCESSINGThe provider is working on it
AUTHORIZEDThe bank approved and reserved the money
SUCCEEDEDThe money was charged
FAILEDThe payment was declined or errored
CANCELEDThe payment was stopped before completing
REFUNDEDAll of the money was returned
PARTIALLY_REFUNDEDSome of the money was returned

Why a database

Stripe already knows about the payment, so why store it again? When Stripe reports “Payment succeeded”, the application needs that fact itself so it can:

  • Mark the order as paid
  • Start shipping
  • Send a receipt
  • Show payment history
  • Allow refunds
  • Handle customer support
  • Perform accounting

So the payment service keeps its own payment records, for example in a payments table:

FieldMeaning
idThe payment's own ID
order_idWhich order
user_idWhich customer
amountMoney amount, in the smallest unit
currencyUSD, EUR, etc.
statusCurrent state
providerStripe
provider_payment_idStripe's ID for the payment
created_atCreation time
updated_atLast update
Example row
id                   pay_123
order_id             order_456
user_id              user_789
amount               10000
currency             USD
status               SUCCEEDED
provider             STRIPE
provider_payment_id  pi_abc123
created_at           ...
updated_at           ...

Now the system knows what happened, even when customer support or reporting needs it later.

Why $100 is stored as 10000

The short answer: avoid storing money as decimal (floating-point) numbers like 100.00. Store whole numbers of cents instead.

1.Computers store decimals approximately

Try writing 1/3 as a decimal: 0.3333333…. The 3s never end, so any written version is slightly off.

Computers have the same problem, just with different numbers. They count in binary (base 2), and in binary, simple-looking numbers like 0.1 and 0.2 never end either. So the computer stores the closest value it can, which is very slightly wrong.

Usually the error is too small to see. But add those numbers, and it shows up:

JavaScript
0.1 + 0.2          // 0.30000000000000004
0.1 + 0.2 === 0.3  // false

2.Why that breaks money

Imagine a cart with three items at $0.10 each. The total should be exactly $0.30:

Dollars as decimals

Item 10.10
Item 20.10
Item 30.10
Total0.30000000000000004
  • The total is 0.30000000000000004, not 0.3.
  • A check like total === 0.30 fails, so the order can look unpaid.
  • Across millions of payments, tiny errors add up to real money.

Cents as integers

Item 110
Item 210
Item 310
Total30
  • Whole numbers are stored exactly, so 10 + 10 + 10 is always 30.
  • 30 cents is shown to the customer as $0.30.

3.The fix: count in cents

Represent USD in cents, the smallest unit, so every amount is a whole number:

PriceStored as
$1.00100
$10.501050
$100.0010000

Calculations then use integers only:

10000 + 5000 = 15000   // $100 + $50 = $150

Convert back to dollars only when showing the amount to a person: 15000 cents is displayed as $150.00.

4.Not every currency has cents

Not every currency has exactly two decimal places. Some have zero and some have three, so a production system needs rules for each currency:

CurrencyDecimal places100 units stored as
USD (US dollar)210000
JPY (Japanese yen)0100
KWD (Kuwaiti dinar)3100000