The actual state machine of a charge — authorize, capture, settle — implemented in Spring Boot, why authorize and capture are deliberately separate steps, and how the payment state machine models every transition explicitly.
Published September 23, 2026
1. AUTHORIZE: verify the card is valid and has sufficient funds; funds are PLACED ON HOLD,
not yet moved. The customer sees a 'pending' charge on their statement.
2. CAPTURE: actually collect the authorized funds (can be less than or equal to the
authorized amount — e.g. an order that shipped partially).
3. SETTLE: the payment processor transfers the captured funds to the merchant's account
(typically batched, happens on the processor's schedule, not synchronously with capture).
A naive implementation treats "charge the card" as one atomic step. Real payment processors separate authorize from capture deliberately, and this separation is a requirement, not an optional detail: it's what lets an e-commerce order authorize funds at checkout (confirming the card is valid and has funds) while deferring the actual capture until the order actually ships — if the order is cancelled before shipping, the authorization simply expires/releases, with no capture and no refund ever needed.
Capturing immediately at checkout (skipping the authorize-then-capture separation) means any post-checkout cancellation requires an actual refund — a separate, visible, sometimes-delayed operation from Payment — Requirements' "no true undo" point. Authorize-then-capture avoids ever needing a refund for the common "cancelled before fulfillment" case, which is a real, meaningful reduction in refund volume and the operational overhead that comes with it.
enum PaymentStatus { CREATED, AUTHORIZED, CAPTURED, SETTLED, FAILED, VOIDED, REFUNDED }
@Document(collection = "payments")
class Payment {
@Id String id;
String orderId;
BigDecimal amount;
PaymentStatus status;
String idempotencyKey; // see Payment — Idempotency Implementation
String processorReference; // the payment processor's own transaction ID — essential for reconciliation
List<PaymentStateTransition> history; // every transition, timestamped — see Requirements' auditability point
}
@Service
class PaymentService {
@Transactional
public Payment authorize(PaymentRequest request) {
// 1. validate request, check idempotency key first (Idempotency Implementation)
// 2. call processor's authorize API
// 3. persist Payment with status=AUTHORIZED and the processor's reference ID
// 4. record the transition in history
}
@Transactional
public Payment capture(String paymentId) {
Payment p = repository.findById(paymentId).orElseThrow();
if (p.getStatus() != PaymentStatus.AUTHORIZED) {
throw new IllegalStateException("Cannot capture from status: " + p.getStatus());
// guarding invalid transitions explicitly — this is what makes the state machine real,
// not just a status field anyone can set to anything
}
// call processor's capture API, update status=CAPTURED, record transition
}
}
The critical implementation discipline: every transition must be validated against the current state before being allowed (you cannot capture a payment that was never authorized, you cannot refund a payment that was never captured) — this is what makes it a genuine state machine rather than a status field anyone can overwrite arbitrarily, and it's the guard that catches bugs (a duplicate capture call, a capture attempted after a void) before they cause a real financial inconsistency.
processorReference is essential, not optionalEvery call to the payment processor returns its own transaction/reference ID — storing this alongside your own Payment record is what makes reconciliation (Payment — Failure Handling & Reconciliation) possible at all: when your records and the processor's records need to be compared to catch discrepancies, the processorReference is the join key between "what we think happened" and "what the processor's records say actually happened."
Q: What happens if capture is attempted after the authorization has expired (most processors expire holds after a set window, e.g. 7 days)? A: The capture call fails at the processor level, and the correct handling is a fresh authorization (a new hold) rather than treating the original authorization as still valid — this is exactly why the state machine needs an explicit path for authorization expiry, not just success/failure.
Q: Can capture ever be for MORE than the originally authorized amount? A: No, standard processor behavior caps a capture at the authorized amount (sometimes slightly more, a small tolerance percentage, depending on the processor) — a capture request exceeding that needs a fresh authorization for the additional amount, not a single oversized capture call.
Q: Why store the full transition history rather than just the current status? A: Beyond auditability (Payment — Requirements), the history is what lets you debug a specific payment's actual timeline during a dispute or reconciliation discrepancy — a bare current-status field with no history loses exactly the information needed to answer 'what happened, and when' after the fact.
Q: Is the authorize/capture separation relevant for payment methods that don't support holds (some digital wallets charge immediately)? A: The state machine can still model it — for a processor that charges immediately, AUTHORIZED and CAPTURED effectively happen together (the capture step becomes a formality that always immediately follows authorization) — the state machine's value is in enforcing valid transitions and auditability regardless of whether the underlying processor supports a genuine two-step hold.