Series MapLesson 22 / 64
Build CoreOrdered learning track

Learn Java Payment Systems Part 022 Available Pending Settled Balances

11 min read2187 words
PrevNext
Lesson 2264 lesson track1335 Build Core

title: Build From Scratch: Large Production Grade Java Payment Systems - Part 022 description: Modeling available, pending, settled, reserved, and payout balances in a production Java payment system using double-entry ledger accounts, holds, reservations, settlement transitions, and strongly consistent balance controls. series: learn-java-payment-systems seriesTitle: Build From Scratch: Large Production Grade Java Payment Systems order: 22 partTitle: Available, Pending, Settled Balances tags:

  • java
  • payments
  • ledger
  • balances
  • settlement
  • payout
  • fintech
  • postgresql date: 2026-07-02

Part 022 — Available, Pending, Settled Balances

A production payment platform does not have one balance.

It has many balances that answer different questions.

How much has the customer authorized?
How much has the provider captured?
How much has the provider settled to us?
How much do we owe the merchant?
How much can the merchant withdraw now?
How much must be held in reserve?
How much is payout pending?
How much is disputed?
How much is frozen by risk?

If your system only has:

merchant.balance

then it is not yet a payment ledger.

It is a number waiting to become an incident.

This part builds the mental model and implementation design for available, pending, settled, reserved, and payout balances.


1. The Core Misunderstanding

Many engineers model balance like this:

balance = total successful payments - refunds - payouts

That is dangerously incomplete.

A successful payment does not always mean cash is settled.

A captured card payment may still be waiting for settlement.

A bank transfer may be received before it is matched.

A QR payment may be paid but not yet included in provider settlement report.

A merchant may be owed money but not allowed to withdraw it yet.

A fraud hold may block payout even though the ledger says funds are payable.

A chargeback may arrive after payout and create recoverable debt.

So we need multiple financial buckets.


2. Balance Buckets

At minimum, a payment platform needs these concepts:

BucketMeaning
authorizedcustomer approved but not captured/posted as receivable
pendingexpected money or merchant payable not yet eligible for payout
settledfunds confirmed by settlement/cash movement
availablefunds eligible for next action, such as payout or spend
reservedfunds held for risk, rolling reserve, disputes, or obligations
payout pendingfunds already locked for payout instruction
disputedfunds affected by chargeback/dispute lifecycle
frozenadministratively blocked due to risk/compliance
suspenseunmatched or unexplained funds

The most important distinction:

pending is about expectation.
settled is about confirmed money movement.
available is about permission to use.

They are related but not equal.


3. Do Not Store Buckets as Status Flags Only

This is weak:

merchant_balance(
  merchant_id,
  pending_amount,
  available_amount,
  settled_amount
)

It may work for a dashboard.

It is weak as financial truth.

Better:

Each bucket is represented by ledger accounts.

Example merchant accounts:

Merchant Pending Payable
Merchant Available Payable
Merchant Reserve Held
Merchant Payout Pending
Merchant Dispute Held
Merchant Negative Receivable

A movement between buckets is a journal.

Move pending -> available
Debit  Merchant Pending Payable
Credit Merchant Available Payable

That transfer is explainable.

A naked field update is not.


4. Balance as Account Meaning

A balance is not just an amount.

It is:

owner + account type + bucket + currency + rail/provider + constraints

Example account identity:

owner_type: MERCHANT
owner_id: m_123
account_type: LIABILITY
bucket: AVAILABLE_PAYABLE
currency: IDR
provider_code: ADYEN
payment_method_type: CARD

This lets you answer:

How much IDR card settlement from provider ADYEN is available for merchant m_123?

Not all businesses need provider-level balance segmentation.

But many payment platforms eventually do because settlement files, provider reserves, fees, and disputes are provider-specific.

Design the account model so the dimension can exist.


5. Lifecycle of Merchant Funds

A simplified card capture flow:

But remember:

payment state machine != ledger account movement

Payment state explains workflow.

Ledger movement explains money.

They must correlate, but they are not the same table.


6. Example: Capture Creates Pending Payable

When a payment is captured, the platform may not yet have received settlement cash.

But it now expects money from the provider and owes the merchant after conditions are met.

Journal:

PAYMENT_CAPTURE_POSTED

Debit  Provider Receivable          100000 IDR
Credit Merchant Pending Payable     100000 IDR

Meaning:

We expect to receive 100000 from provider.
We owe merchant 100000, but not available yet.

This is not yet available.

Why?

Because provider settlement can fail, be delayed, be netted, or include adjustments.


7. Example: Settlement Moves Receivable to Cash

When settlement file or bank statement confirms receipt:

PAYMENT_SETTLEMENT_RECEIVED

Debit  Bank Settlement Cash         100000 IDR
Credit Provider Receivable          100000 IDR

This says:

Expected provider money became actual cash/settlement cash.

It still does not automatically make merchant balance available.

Availability may depend on:

  • settlement confirmation
  • risk window
  • merchant settlement schedule
  • rolling reserve
  • refund reserve
  • dispute exposure
  • compliance hold
  • payout minimum
  • business cutoff

8. Example: Pending Becomes Available

After settlement and eligibility checks:

MERCHANT_FUNDS_RELEASED

Debit  Merchant Pending Payable     95000 IDR
Credit Merchant Available Payable   95000 IDR

If a 5% reserve is held:

MERCHANT_FUNDS_RELEASED_WITH_RESERVE

Debit  Merchant Pending Payable     100000 IDR
Credit Merchant Available Payable    95000 IDR
Credit Merchant Reserve Held          5000 IDR

This journal still balances because all entries are liabilities and debit/credit sides reduce/increase the normal balance according to account type.

Operational meaning:

Merchant can withdraw 95000.
Platform still owes 5000 but it is reserved.

9. Available Is a Permissioned Balance

Available balance is not merely settled balance.

available = settled - reserved - payout_pending - frozen - operational holds

But in a ledger design, we do not compute this from one formula every time.

We move money between explicit accounts:

Merchant Available Payable
Merchant Reserve Held
Merchant Payout Pending
Merchant Dispute Held
Merchant Frozen Payable

The available account itself is the answer.

This is more auditable than dynamic subtraction across multiple tables.


10. Reserve Balance

A reserve is money owed to the merchant but held by the platform.

Common reasons:

  • rolling reserve
  • fraud risk
  • new merchant probation
  • high chargeback rate
  • compliance review
  • refund exposure
  • dispute buffer

Posting reserve at release time:

Debit  Merchant Pending Payable
Credit Merchant Available Payable
Credit Merchant Reserve Held

Releasing reserve later:

Debit  Merchant Reserve Held
Credit Merchant Available Payable

Reserve is not revenue.

It is still a liability.

Do not treat reserve as platform income.


11. Payout Reservation

When merchant requests payout, do not immediately reduce available balance and hope the transfer succeeds.

Move funds into payout pending.

MERCHANT_PAYOUT_RESERVED

Debit  Merchant Available Payable   95000 IDR
Credit Merchant Payout Pending      95000 IDR

Then create payout instruction.

If payout succeeds:

MERCHANT_PAYOUT_SETTLED

Debit  Merchant Payout Pending      95000 IDR
Credit Bank Settlement Cash         95000 IDR

If payout fails and funds should return:

MERCHANT_PAYOUT_FAILED_RELEASED

Debit  Merchant Payout Pending      95000 IDR
Credit Merchant Available Payable   95000 IDR

This prevents double payout.

The moment funds move from available to payout pending, another payout request cannot use them.


12. Strongly Consistent Reservation

Reservation must be atomic.

check available balance
move available -> payout pending
create payout record
commit

In SQL terms, lock the balance row or use conditional update.

Pessimistic approach:

select normal_balance_minor
from account_balance
where account_id = :available_account_id
for update;

Then verify amount.

Conditional update approach:

update account_balance
set
    normal_balance_minor = normal_balance_minor - :amount,
    version = version + 1,
    updated_at = now()
where account_id = :available_account_id
  and normal_balance_minor >= :amount;

If affected rows = 0, insufficient available balance.

But remember:

The actual financial movement should still be represented as a journal.

Do not let conditional balance update bypass the ledger.

The safe path is one transaction:

lock/read balance
validate sufficient available
post reservation journal
update balance projection from journal
insert payout instruction
commit

13. Balance Projection Table for Buckets

The account model from Part 021 already supports bucketed balance.

For convenient reads, expose a balance view:

create view merchant_balance_view as
select
    a.owner_id as merchant_id,
    a.currency_code,
    sum(case when a.balance_bucket = 'PENDING_PAYABLE'
        then b.normal_balance_minor else 0 end) as pending_minor,
    sum(case when a.balance_bucket = 'AVAILABLE_PAYABLE'
        then b.normal_balance_minor else 0 end) as available_minor,
    sum(case when a.balance_bucket = 'RESERVE_HELD'
        then b.normal_balance_minor else 0 end) as reserve_minor,
    sum(case when a.balance_bucket = 'PAYOUT_PENDING'
        then b.normal_balance_minor else 0 end) as payout_pending_minor,
    sum(case when a.balance_bucket = 'DISPUTE_HELD'
        then b.normal_balance_minor else 0 end) as dispute_minor
from ledger_account a
join account_balance b on b.account_id = a.account_id
where a.owner_type = 'MERCHANT'
group by a.owner_id, a.currency_code;

This view is derived.

It is not the write model.


14. Negative Balance Policy

Some accounts must never go negative.

Examples:

customer wallet available balance
merchant available payable before payout
merchant reserve held
payout pending

Some accounts can become negative by business design:

merchant negative receivable after chargeback
platform fee receivable
provider receivable during timing gap
suspense during unmatched imports

Do not use one universal rule.

Model it at account policy level.

alter table ledger_account
add column allow_negative boolean not null default false;

Then the ledger posting service checks projected balances for constrained accounts before committing.

for (ProjectedAccountDelta delta : draft.projectedDeltas()) {
    Account account = accountRepository.get(delta.accountId());
    Balance current = balanceRepository.getForUpdate(delta.accountId());
    Balance next = current.apply(delta);

    if (!account.allowNegative() && next.normalBalanceMinor() < 0) {
        throw new InsufficientBalanceException(account.accountId(), current, delta);
    }
}

This is payment-specific concurrency control.


15. Pending Does Not Always Mean Same Thing

Pending is overloaded.

Avoid one global PENDING bucket.

Separate meanings:

Pending TypeMeaning
provider receivable pendingprovider owes platform but not settled
merchant pending payableplatform owes merchant but not available
payout pendingpayout instruction created but not settled
refund pendingrefund initiated but not confirmed
dispute pendingdispute open, outcome unknown
reconciliation pendingprovider/bank evidence not matched

Use specific account buckets.

PENDING_PAYABLE
PAYOUT_PENDING
DISPUTE_HELD
UNMATCHED_SETTLEMENT_SUSPENSE
REFUND_PENDING_CLEARING

Generic names hide financial meaning.


16. Refund From Available vs Pending

Refunds are subtle.

Suppose merchant has captured payment, but funds are not yet available.

Refund before settlement:

REFUND_POSTED_BEFORE_RELEASE

Debit  Merchant Pending Payable     100000 IDR
Credit Provider Receivable          100000 IDR

Meaning:

We no longer expect the provider to settle this amount.
We no longer owe the merchant this pending amount.

Refund after merchant funds became available:

REFUND_POSTED_AFTER_RELEASE

Debit  Merchant Available Payable   100000 IDR
Credit Refund Clearing Payable      100000 IDR

Then provider/bank confirms refund movement:

REFUND_SETTLED

Debit  Refund Clearing Payable      100000 IDR
Credit Bank Settlement Cash         100000 IDR

If merchant already withdrew the money, the refund may create merchant negative receivable:

REFUND_FUNDED_BY_PLATFORM

Debit  Merchant Negative Receivable 100000 IDR
Credit Bank Settlement Cash         100000 IDR

Do not force all refunds into one posting rule.

The funding source matters.


17. Partial Capture and Available Amount

Authorization amount is not the same as captured amount.

Example:

Authorized: 100000 IDR
Captured:    70000 IDR
Cancelled:   30000 IDR

Ledger should only post captured financial receivable/payable.

Do not create merchant pending payable from full authorization unless your business genuinely owes it.

A card authorization is often a permission/hold, not final payable money.

Represent authorization in payment state and optionally operational hold records.

Represent captured/settled money in ledger entries.


18. Customer Wallet Balances

Wallets are stricter because the available balance is customer-facing stored value.

Top up initiated:

No wallet liability yet if funds are not confirmed.

Top up confirmed:

WALLET_TOPUP_CONFIRMED

Debit  Bank Settlement Cash         100000 IDR
Credit Customer Wallet Available    100000 IDR

Spend from wallet:

WALLET_PAYMENT_RESERVED

Debit  Customer Wallet Available    100000 IDR
Credit Customer Wallet Hold         100000 IDR

Capture spend:

WALLET_PAYMENT_CAPTURED

Debit  Customer Wallet Hold         100000 IDR
Credit Merchant Pending Payable     100000 IDR

Release failed reservation:

WALLET_HOLD_RELEASED

Debit  Customer Wallet Hold         100000 IDR
Credit Customer Wallet Available    100000 IDR

Wallet balance must not go negative unless you intentionally support credit.

Most payment wallets should not.


19. Holds vs Ledger Reservations

A hold can be represented in two ways.

Option A — Operational Hold Table

create table balance_hold (
    hold_id uuid primary key,
    account_id uuid not null references ledger_account(account_id),
    amount_minor bigint not null check (amount_minor > 0),
    currency_code char(3) not null,
    reason_code varchar(120) not null,
    status varchar(32) not null check (status in ('ACTIVE', 'RELEASED', 'CONSUMED', 'EXPIRED')),
    expires_at timestamptz,
    created_at timestamptz not null default now(),
    released_at timestamptz
);

Then available is calculated:

available = balance - active holds

Option B — Ledger Account Movement

Debit  Available
Credit Hold

For payment-critical reservations, prefer ledger account movement.

For temporary operational UI holds, a hold table may be acceptable.

Rule of thumb:

If the hold controls money movement, represent it in the ledger.
If the hold is purely operational metadata, it may live outside but must be auditable.

20. Settlement Lag Model

Payment platforms live in timing gaps.

Each delay needs explicit state.

Do not pretend capture means settlement.

Do not pretend settlement means availability.

Do not pretend availability means payout already happened.


21. Balance Read API

A payment platform should expose balances with explicit semantics.

Bad response:

{
  "merchantId": "m_123",
  "balance": 95000
}

Better:

{
  "merchantId": "m_123",
  "currency": "IDR",
  "pending": {
    "amountMinor": 100000,
    "meaning": "Captured or expected funds not eligible for payout"
  },
  "available": {
    "amountMinor": 95000,
    "meaning": "Eligible for payout subject to final operational controls"
  },
  "reserve": {
    "amountMinor": 5000,
    "meaning": "Merchant-owned funds held by platform policy"
  },
  "payoutPending": {
    "amountMinor": 0,
    "meaning": "Funds locked for payout instructions not yet settled"
  },
  "asOf": "2026-07-02T09:00:00Z"
}

Never expose ambiguous balance semantics to merchants or operators.

Ambiguity becomes support tickets, disputes, and financial mistakes.


22. Balance Mutation API

External clients should not mutate balances directly.

They trigger domain commands:

capture payment
release merchant funds
create payout
open dispute
release reserve
post adjustment

Internal APIs should also avoid:

POST /balances/{id}/increment

Use:

POST /payouts
POST /ledger-adjustment-requests
POST /reserve-releases

The domain command decides the correct posting rule.

The ledger posts the entries.

The balance projection follows.


23. Balance Eligibility Engine

Availability is not purely accounting.

It is accounting plus policy.

Eligibility checks may include:

  • merchant status active
  • KYB complete
  • payout capability enabled
  • risk hold absent
  • minimum payout amount reached
  • settlement received
  • rolling reserve applied
  • settlement schedule reached
  • currency supported
  • bank account verified
  • no compliance freeze
  • dispute ratio under threshold

Do not hide all this inside ledger.

Ledger says:

What is owed and where is it bucketed?

Policy engine says:

Can this owner use/move the available funds now?

Both are needed.


24. Snapshot and Rebuild

Balances used for money movement need rebuildability.

Daily process:

1. close previous business day
2. run journal balance check
3. run trial balance
4. compare projection against reconstructed sums
5. store account balance snapshots
6. produce finance report

Projection rebuild algorithm:

public void rebuildAccountBalance(AccountId accountId) {
    Balance reconstructed = entryRepository.sumEntriesForAccount(accountId);
    balanceRepository.replaceProjection(accountId, reconstructed);
    integrityRepository.recordProjectionRebuild(accountId, reconstructed);
}

For large ledgers, rebuild from last snapshot:

last snapshot + entries after snapshot = current projection

25. Reconciliation Impact on Balances

Reconciliation can change balance buckets.

Examples:

Provider Settled Less Than Expected

Expected:

Provider Receivable 100000

Settlement file says:

Only 99000 received due to provider fee or adjustment.

You need posting rule that explains the difference:

Debit  Bank Settlement Cash          99000
Debit  Provider Fee Expense           1000
Credit Provider Receivable          100000

Unknown Extra Bank Credit

Debit  Bank Settlement Cash          100000
Credit Settlement Suspense           100000

Then later match and reclassify suspense.

Suspense is not failure.

Suspense is honest accounting for unknown evidence.


26. Dispute and Chargeback Impact

Dispute opened:

DISPUTE_FUNDS_HELD

Debit  Merchant Available Payable   100000 IDR
Credit Merchant Dispute Held        100000 IDR

Dispute won:

DISPUTE_WON_RELEASED

Debit  Merchant Dispute Held        100000 IDR
Credit Merchant Available Payable   100000 IDR

Dispute lost:

DISPUTE_LOST_BOOKED

Debit  Merchant Dispute Held        100000 IDR
Credit Bank Settlement Cash         100000 IDR

If funds were already paid out:

DISPUTE_LOST_AFTER_PAYOUT

Debit  Merchant Negative Receivable 100000 IDR
Credit Bank Settlement Cash         100000 IDR

This shows why one balance field is not enough.


27. Balance Monitoring

Business SLOs for balances:

No account with negative balance unless allow_negative=true.
No wallet available account negative.
No payout created without reservation journal.
No payout pending older than allowed SLA without investigation.
No suspense balance above threshold without open reconciliation case.
No merchant reserve release without policy decision.
Projection must match reconstructed ledger within zero tolerance.

Metrics:

ledger.account.negative.count
ledger.balance.projection_mismatch.count
ledger.payout.reservation.failure.count
ledger.suspense.amount_minor.by_currency
ledger.reserve.amount_minor.by_merchant_risk_tier
ledger.payout_pending.age.max
ledger.available_balance.rebuild.duration

Financial correctness needs metrics, not only logs.


28. Java Domain Model

public enum BalanceBucket {
    PENDING_PAYABLE,
    AVAILABLE_PAYABLE,
    RESERVE_HELD,
    PAYOUT_PENDING,
    DISPUTE_HELD,
    FROZEN_PAYABLE,
    NEGATIVE_RECEIVABLE,
    SUSPENSE
}

public record AccountBalance(
    AccountId accountId,
    CurrencyCode currency,
    long normalBalanceMinor,
    long debitPostedMinor,
    long creditPostedMinor,
    long version
) {
    public AccountBalance requireSufficient(long amountMinor) {
        if (normalBalanceMinor < amountMinor) {
            throw new InsufficientBalanceException(accountId, normalBalanceMinor, amountMinor);
        }
        return this;
    }
}

Payout reservation service:

public Payout reserveForPayout(CreatePayoutCommand command) {
    return transactionTemplate.execute(tx -> {
        AccountId available = accountResolver.merchantAvailable(
            command.merchantId(),
            command.currency()
        );

        AccountBalance balance = balanceRepository.getForUpdate(available);
        balance.requireSufficient(command.amountMinor());

        PostedJournal reservation = ledger.post(
            PayoutPostingCommands.reserve(command)
        );

        Payout payout = payoutRepository.createPending(command, reservation.journalId());
        outbox.publish(PayoutEvents.created(payout));

        return payout;
    });
}

The service does not update balance manually.

It posts a ledger movement.


29. Common Bugs

Bug 1 — Making Funds Available on Capture

This allows payout before settlement.

Only do it if your business intentionally advances funds and accounts for that credit risk.

Bug 2 — Releasing Reserve by Deleting Hold Row

If reserve is financial, release must be a journal.

Bug 3 — Payout Without Reservation

Two payout requests can spend the same available balance.

Always reserve.

Bug 4 — Negative Wallet Balance Due to Race

Use row lock/conditional reservation and ledger posting in one transaction.

Bug 5 — Treating Chargeback as Refund

Chargeback has different evidence, deadlines, fees, and liability.

Use dispute/chargeback-specific accounts.

Bug 6 — Hiding Suspense

Unmatched money should go to suspense, not be ignored.

Suspense must be visible and aged.


30. Test Matrix

ScenarioExpected Result
capture paymentprovider receivable increases, merchant pending payable increases
settlement receivedbank cash increases, provider receivable decreases
release with reservepending decreases, available + reserve increase
payout reservationavailable decreases, payout pending increases
payout successpayout pending decreases, bank cash decreases
payout failurepayout pending decreases, available increases
refund before releasepending payable and provider receivable reverse
refund after releaseavailable decreases or merchant receivable increases
reserve releasereserve decreases, available increases
dispute openedavailable decreases, dispute held increases
dispute wondispute held decreases, available increases
dispute lost after payoutmerchant negative receivable increases
duplicate payout requestidempotency returns existing reservation/payout
concurrent payout requestsonly one can reserve insufficient shared funds
projection rebuildprojection equals sum of entries

31. Production Checklist

Before enabling merchant payout or wallet spend:

  • balances are represented by ledger accounts, not only mutable fields
  • pending, available, reserve, payout pending, dispute, and suspense are distinct
  • payout requires reservation journal
  • reservation is atomic with payout creation
  • constrained accounts cannot go negative
  • refund funding source is modeled
  • reserve is liability, not revenue
  • settlement does not automatically imply availability without policy
  • availability semantics are explicit in API responses
  • reconciliation can post suspense and reclassification journals
  • chargeback after payout can create merchant receivable/debt
  • projection can be rebuilt from immutable entries
  • balance monitoring exists
  • operators can explain any balance number through journal history

32. Mental Model to Keep

Balances are not just numbers.

They are promises with conditions.

Pending = we expect or owe, but not yet usable.
Settled = cash/evidence confirmed.
Available = usable under policy.
Reserved = owned but blocked.
Payout pending = already committed to outbound movement.
Suspense = real evidence not yet explained.

A production payment system does not ask:

What is the balance?

It asks:

Which balance, for whom, in which currency, under which condition, backed by which journal entries, and available for which action?

That is the standard you should hold yourself to.


References

Lesson Recap

You just completed lesson 22 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.