Section 19

Banking & payments domain

The Australian payment rails (NPP, PayTo, BECS, cards, RTGS and SWIFT), Open Banking under the CDR, the regulations that shape a design (CPS 234, CPS 230, AML/CTF, PCI DSS) and the ledger basics underneath it all. Generic, public information only. Message formats are covered in 2 · Data formats.

19.1 The payments landscape in Australia Added

Several rails move money between banks. Each one clears (swaps payment messages) and then settles (moves real money between the banks' accounts at the RBA).

📨 Clearing

The banks exchange payment instructions and work out who owes whom. No central-bank money moves yet.

🏦 Settlement

Funds move between the banks' Exchange Settlement Accounts (ESAs) at the RBA, in RITS (Australia's RTGS system). Settlement is final.

⚖️ Gross vs net

Gross: each payment settles on its own (NPP, high-value). Net: many payments are added up and only the difference settles (BECS, cards).

RailWho runs itSpeedSettlementTypical use
NPP (Osko, PayID, PayTo)Australian Payments Plus (AP+)Seconds, 24/7Each payment, in real time, in the RBA's Fast Settlement Service (FSS)Pay anyone, bills, payroll on demand, PayTo debits
BECS / Direct EntryAdministered by AusPayNetHours to next day (business days)Net, in batches, in RITSPayroll, welfare, direct debits, bulk payments
BPAYAP+Business daysBatchBill payments (biller code + reference)
Cards: eftpos, Visa, Mastercardeftpos is part of AP+; Visa and Mastercard are international schemesAuthorisation in seconds; clearing in batchesNet, in batchesRetail purchases, online checkout
HVCS (high value)AusPayNet administers it; RBA runs RITSReal time, during RITS operating hoursGross (RTGS), one by one in RITSLarge interbank and corporate payments
SWIFT (cross-border)Swift network + correspondent banksMinutes to daysAcross correspondent (nostro/vostro) accountsInternational payments
  • Clearing = exchanging messages. Settlement = moving central-bank money. Interviewers love this distinction.
  • Real-time gross settlement removes credit risk between banks. That is why NPP funds can be released immediately.
  • Most "payment platforms" in a bank are adapters between channels, the core ledger and several rails, each with different speed, format and cut-offs.

Clearing vs settlement: the payments question that trips people up 🏦

📨 Clearing = banks swap payment messages and agree who owes whom

💰 Settlement = real money moves between the banks' accounts at the central bank (RITS in Australia)

⚡ NPP settles every payment on its own, in seconds, 24/7

📦 BECS and cards net thousands of payments and settle the difference in batches

If you design payment systems, know which one your rail does. It changes your risk, your cut-offs and your reconciliation.

#Payments #Banking #NPP #SolutionArchitecture

19.2 NPP, Osko & PayID Added

The New Payments Platform is Australia's fast-payment rail: account-to-account payments in seconds, 24/7, with rich ISO 20022 data and simple PayID addressing.

⚡ Real time, 24/7

Launched to the public in February 2018. Payments clear and settle in seconds, every day of the year.

🏦 Fast Settlement Service

The RBA built the FSS as part of RITS. It settles each NPP payment individually, 24/7, across the banks' ESAs.

📇 PayID

Pay a phone number, email, ABN or organisation ID instead of a BSB and account number. The payer sees the account name before paying.

🧩 Overlay services

Products built on the NPP's core rails. Osko (by BPAY) was the first; PayTo (19.3) is another.

🧾 ISO 20022 data

Credit transfers (pacs.008) and status reports (pacs.002) carry far richer remittance data than Direct Entry's 18 characters.

🛡️ Confirmation of Payee

Since July 2025, banks check the name against the BSB and account number and show match / close match / no match before you pay.

Typical bank integration (generic)

ComponentJobDesign points
Channel + API gatewayAuthenticate the customer, accept the requestIdempotency key per request, step-up auth for new payees
Payments hubOrchestrate, map to ISO 20022, route to the right railState machine per payment, outbox for events, one canonical model
Fraud / sanctionsScore in real time before the money goesSub-second budget. NPP funds are available to the payee almost instantly, so you can't "catch it tomorrow"
NPP gatewayConnect to the NPP (directly, or through a sponsor bank or aggregator)Timeouts, circuit breakers, no blind retries of a send
Core bankingDebit the payer, credit the payee (inbound)Must answer 24/7. Use stand-in or shadow ledgers if the core has a batch window
LiquidityKeep enough money in the FSS allocation of the ESAMonitor and top up, including weekends
  • 24/7 real time removes your batch window: the core ledger, fraud and monitoring all need weekend-grade availability.
  • A timeout on a send is "unknown", not "failed". Query the status by end-to-end ID before you retry.
  • For a worked event-driven example, see the notification platform in 20.5.
How to implement it
NeedLibrary / APINotes
ISO 20022 classesdotnet-xscgen (XmlSchemaClassGenerator) or xsd.exeGenerate C# classes from the official XSDs for pacs.008, pacs.002. There is no official Microsoft ISO 20022 package
Read / write / validatebuilt-in XmlSerializer, XDocument, XmlSchemaSetValidate every inbound message against the scheme's XSD before you act on it (see 2 · formats)
Gateway callsMicrosoft.Extensions.Http.ResilienceTimeouts and a circuit breaker on the scheme connection. Avoid automatic retries of a payment send
Payment eventsAzure.Messaging.ServiceBus, Confluent.KafkaPublish PaymentSent / PaymentReceived through an outbox for notifications, fraud and analytics
// Scheme gateway client: fail fast, never blindly retry a send
builder.Services.AddHttpClient<NppGatewayClient>(c => c.BaseAddress = new(gatewayUrl))
    .AddResilienceHandler("npp-gateway", pipeline =>
    {
        pipeline.AddTimeout(TimeSpan.FromSeconds(5));          // whole attempt
        pipeline.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
        {
            FailureRatio = 0.5,
            SamplingDuration = TimeSpan.FromSeconds(30),
            BreakDuration = TimeSpan.FromSeconds(15)
        });
        // No retry strategy: on timeout, query status by end-to-end ID instead
    });

Keep the end-to-end ID (and your idempotency key) on the payment record from the start. It is what lets you safely find out what happened after a timeout.

What actually happens when you "pay a PayID" in Australia? ⚡

1️⃣ Your bank looks up the PayID and shows you the account name

2️⃣ Fraud scoring runs in milliseconds (there's no "tomorrow" to catch it)

3️⃣ An ISO 20022 pacs.008 goes over the NPP to the payee's bank

4️⃣ The RBA's Fast Settlement Service settles that one payment, 24/7

5️⃣ The payee has the money in seconds

For architects, the hard part isn't the message. It's making every system behind it available at 3 am on a Sunday.

#NPP #RealTimePayments #ISO20022 #Banking

19.3 PayTo Added

A digital payment agreement (mandate) that the customer authorises in their own banking app, so a business can pull payments over the NPP in real time.

Old: BECS direct debit

  • Customer signs a Direct Debit Request with the business
  • The bank never sees the mandate
  • Bad account details found days later (dishonour)
  • Hard for customers to see or stop

New: PayTo

  • Agreement stored centrally in the NPP's Mandate Management Service (MMS)
  • Customer authorises it in their banking app
  • Payments settle in real time
  • Customer can pause, amend or cancel any time
  • The bank, not the merchant, holds the source of truth for consent, so the customer's control is real.
  • Treat an agreement as a state machine (created, active, paused, cancelled) and react to MMS change notifications quickly.
  • A payment request can be declined in real time (agreement paused, limit exceeded). Design the merchant flow for an instant "no".
How to implement it
NeedLibrary / APINotes
Agreement storebuilt-in EF Core with a status enum + concurrency tokenKeep the MMS agreement ID as a unique key. Reject illegal status changes in the domain model
Change notificationsbuilt-in ASP.NET Core minimal APIs + Azure.Messaging.ServiceBusReceive pause/cancel/amend updates from your PayTo provider, queue them, process idempotently
Provider APIsIHttpClientFactory + Microsoft.Extensions.Http.ResilienceThere is no official .NET PayTo SDK. You call your sponsor bank's or payment provider's REST API

Direct debit is getting a 21st-century upgrade in Australia 🔄

✅ PayTo agreements are authorised in YOUR banking app, not on a paper form

⚡ Payments settle in real time over the NPP

⏸️ You can pause or cancel any agreement yourself

🔁 Businesses can migrate existing direct debits with notice

The design shift: consent moves from the merchant's database to a central mandate service the customer controls.

#PayTo #Payments #OpenFinance #Australia

19.4 BECS / Direct Entry & batch payments Added

The Bulk Electronic Clearing System carries batch payments (payroll, welfare, direct debits) as fixed-width ABA files, exchanged and net-settled on business days. It is still Australia's largest retail payment system by value.

ABA recordWhat it holdsWhy it matters
Type 0 descriptiveBank, user name and ID, description, processing dateOne per batch. Wrong user ID = whole file rejected
Type 1 detailBSB, account, transaction code, amount in cents, name, 18-char lodgement reference, trace BSB/accountOne per payment. Trace details tell the receiver where to send returns
Type 7 file totalNet, credit and debit totals, record countControl totals: the bank rejects the file if they don't match the detail records

Every record is exactly 120 characters, separated by CRLF. Some banks also require a balancing (contra) record that debits the sender's own account.

⏰ Cut-offs

Each bank sets its own lodgement cut-offs. Miss one and the file goes in the next business-day cycle.

↩️ Returns & dishonours

Closed accounts, wrong numbers and insufficient funds come back days later. Your process must handle them.

🔍 Reconciliation

Match by lodgement reference and amount. Check control totals in, statement entries out, returns back.

The planned move away from BECS (public timeline)

In an interview, say "BECS is being wound down over time, but there is currently no fixed end date". Batch file skills will be needed for years.

  • Batch payments fail late: plan for returns, dishonours and re-presentment, not just the happy path.
  • Make file processing idempotent: a re-uploaded file must not pay staff twice (hash the file, track batch IDs).
  • Moving a flow from BECS to NPP changes the error model from "fails tomorrow" to "fails now", which is usually better.
How to implement it
NeedLibrary / APINotes
Fixed-width parsingbuilt-in File.ReadLines / StreamReader; System.IO.Pipelines for very large filesSlice each 120-char line by position. Simple and fast
Record mappingFileHelpers ([FixedLengthRecord], [FieldFixedLength])MIT-licensed, mature, but updated rarely. CsvHelper is for delimited files, not fixed width
TransferSSH.NETSFTP upload and download to the bank's file gateway
Duplicate filesbuilt-in SHA256.HashDataAsync + a unique indexStore the file hash and batch ID so the same file can't be processed twice
// Check an ABA file's control totals before you send it
long credits = 0, debits = 0; int count = 0;
foreach (var line in File.ReadLines(path))
{
    if (line.Length != 120) throw new FormatException("Each ABA record must be 120 chars");
    switch (line[0])
    {
        case '1':                                        // detail record
            var cents = long.Parse(line.AsSpan(20, 10)); // positions 21-30
            if (line.Substring(18, 2) == "13") debits += cents;   // 13 = debit
            else credits += cents;                                 // 50-57 = credits
            count++;
            break;
        case '7':                                        // file total record
            if (long.Parse(line.AsSpan(30, 10)) != credits ||    // credit total
                long.Parse(line.AsSpan(40, 10)) != debits  ||    // debit total
                int.Parse(line.AsSpan(74, 6)) != count)          // record count
                throw new InvalidDataException("Control totals don't match");
            break;
    }
}

Amounts are whole cents in the file. Parse them as integers (or decimal), never double.

Australia's biggest retail payment system by value is… a 120-character text file format 📄

💼 Most payroll, welfare and direct debits still run on BECS (Direct Entry)

⏰ Bank cut-offs decide whether money lands today or tomorrow

↩️ Errors come back days later as returns and dishonours

📅 The industry dropped its June 2030 end date in Dec 2025, pending a new roadmap

Takeaway: "legacy" batch rails are where much of the money is. Design for reconciliation, not just submission.

#Payments #BECS #Banking #Integration

19.5 Cards: the four-party model Added

A card payment links cardholder → issuer and merchant → acquirer through a scheme. It happens in three steps: authorisation, clearing and settlement.

👤 Cardholder + issuer

The issuer gives out the card, approves or declines, and bills the cardholder.

🏪 Merchant + acquirer

The acquirer signs up the merchant, sends transactions to the scheme and pays the merchant.

🌐 Scheme

eftpos, Visa, Mastercard: rules, routing, network tokens, and working out the net positions.

TopicWhat to know
ISO 8583Binary card message standard for authorisations (format covered in 2 · formats)
Network tokensThe scheme swaps the card number (PAN) for a token tied to one device or merchant, plus a one-time cryptogram. Apple Pay and Google Pay use these. Tokens update when a card is reissued
PSP / vault tokensYour payment provider stores the PAN and gives you a reference. Useful for scope reduction, but only that provider can use it
3-D SecureOnline authentication by the issuer. Most checks are frictionless (risk-based); some need a challenge in the banking app. A successful check usually shifts fraud liability to the issuer
ChargebacksThe cardholder disputes a transaction through the issuer. Scheme rules set reason codes, evidence and deadlines. Keep order, delivery and authentication evidence
Australian contextLeast-cost routing on dual-network debit cards. Since 1 October 2026, eftpos, Mastercard and Visa rules no longer allow surcharges, alongside lower RBA interchange caps

PCI DSS v4.x: shrink the scope

  • Auth ≠ settlement. An authorisation hold can expire or be partly captured. Your ledger must model both (see 19.9).
  • The cheapest PCI control is not touching the PAN at all.
  • Prefer network tokens for stored cards: higher approval rates and automatic card updates.
How to implement it
NeedLibrary / APINotes
ISO 8583No official Microsoft or widely adopted free libraryUsually handled by the card switch, processor or a vendor SDK. Community parsers exist; review them carefully before use
PSP / 3DS / tokensYour PSP's .NET SDK or REST API via IHttpClientFactoryTokenisation, 3-D Secure and network tokens are provider features, not .NET libraries
Keep PAN out of logsMicrosoft.Extensions.Compliance.Redaction + Microsoft.Extensions.TelemetryClassify sensitive fields and redact them in structured logs

Tapping your card takes 1 second. The payment takes days. Here's why 💳

1️⃣ Authorisation: the issuer approves and puts a HOLD on funds (seconds)

2️⃣ Clearing: the merchant sends its batch through the acquirer and scheme (later that day)

3️⃣ Settlement: the scheme nets everyone's positions and the banks settle (days)

🔐 Network tokens mean the merchant never sees your real card number

Design tip: your ledger needs "pending" and "posted" states, or customers will see strange balances.

#Cards #Payments #PCIDSS #Fintech

19.6 Cross-border payments & SWIFT ISO 20022 Added

International payments hop between banks that hold accounts with each other (correspondent banking), using Swift messages that are now ISO 20022 (CBPR+) for payment instructions.

Old (MT)ISO 20022 (MX)Used for
MT103pacs.008Customer credit transfer
MT202 / MT202 COVpacs.009 / pacs.009 COVBank-to-bank transfer / cover payment
Return, rejectpacs.004, pacs.002Returns and status
MT940 / MT950camt.053 (and camt.052, camt.054)Statements and notifications (later roadmap)
  • The big win of ISO 20022 is structured data (names, addresses, purpose), which makes sanctions screening and AML monitoring more accurate.
  • Never truncate. Converting rich ISO data back into an MT-shaped internal model loses data and can break compliance.
  • Store the UETR on every payment so operations can answer "where is my money?".
How to implement it
NeedLibrary / APINotes
Message classesdotnet-xscgen against the CBPR+ / ISO 20022 XSDsSwift's usage guidelines (MyStandards) add rules on top of the base XSD, so validate both
Validationbuilt-in XmlSchemaSet + XmlReaderSettings.ValidationType = SchemaReject invalid messages before they reach screening or posting
Swift connectivityVendor or Swift-provided connectivity; REST calls via HttpClient for Swift APIs (e.g. gpi tracking)Connectivity normally goes through the bank's Swift infrastructure or a service bureau, not a NuGet package

22 November 2025: the day cross-border payments switched languages 🌏

📜 MT103 is out for bank-to-bank payment instructions. pacs.008 is in

🧾 ISO 20022 carries structured names, addresses and purpose codes

🔎 Better data = better sanctions screening and fewer false positives

📍 gpi's UETR lets you track a payment end to end

Lesson for architects: if your internal model is still MT-shaped, you're throwing away the data the migration was for.

#ISO20022 #SWIFT #CrossBorderPayments #Banking

19.7 Open Banking & the Consumer Data Right (CDR) Added

The CDR lets consumers tell their bank to share their data with an accredited data recipient, through standard, highly secured APIs, with consent they can withdraw at any time.

🏦 Data holders

Banks (since July 2020), energy retailers (since 2022) and now non-bank lenders.

📲 Accredited data recipients

Budgeting apps, lenders, comparison and accounting services, accredited by the ACCC.

📏 Rules & standards

Treasury makes the Rules; the Data Standards Body sets the API and security standards; the ACCC and OAIC regulate.

🎛️ Consent dashboards

Both sides show what is shared, for how long, and let the consumer stop it.

AreaCurrent public position (October 2026)
Security profileBuilds on FAPI 1.0 Advanced: PAR with PKCE, JARM responses, private_key_jwt client auth, mTLS with CDR-issued certificates, certificate-bound tokens. FAPI 2.0 alignment has been discussed but is not mandated
Non-bank lendersProduct data from 13 July 2026; consumer data from 9 November 2026 (initial providers) and 10 May 2027 (large providers)
Action initiationThe framework was legislated in August 2024, but no action types (such as payment initiation) have been declared. The Government said it would not rush this while it "resets" the CDR
Data scope2025 rule changes cut the historical data to be shared from 7 years to 2 and removed some niche products

Architecture implications for a data holder

  • CDR security is FAPI-based: think "mTLS everywhere + sender-constrained tokens + signed requests".
  • Consent is a first-class data entity with a lifecycle, not a checkbox.
  • In Australia, account-to-account payment initiation today happens through PayTo, not the CDR.
How to implement it
NeedLibrary / APINotes
Validate access tokensMicrosoft.AspNetCore.Authentication.JwtBearerIssuer, audience, lifetime, signature. Then check the token's certificate binding (cnf)
mTLSMicrosoft.AspNetCore.Authentication.Certificate + Kestrel ClientCertificateModeBehind a TLS-terminating gateway, use certificate forwarding instead
Authorisation serverDuende.IdentityServer commercial licence; OpenIddictDuende supports FAPI 2.0 (certified in v7.3) and PAR. OpenIddict is free; check which FAPI features you need
Client side (PAR)built-in OIDC handler: PushedAuthorizationBehavior.RequireBuilt into ASP.NET Core since .NET 9
builder.WebHost.ConfigureKestrel(k => k.ConfigureHttpsDefaults(
    https => https.ClientCertificateMode = ClientCertificateMode.RequireCertificate));

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(o =>
    {
        o.Authority = "https://auth.bank.example";
        o.Audience = "cdr-banking-api";
        o.MapInboundClaims = false;            // keep "cnf" as is
    });

// Sender-constrained token: the cert on this connection must match the token
app.MapGet("/banking/accounts", async (HttpContext ctx, ClaimsPrincipal user) =>
{
    var cert = await ctx.Connection.GetClientCertificateAsync();
    var cnf = user.FindFirst("cnf")?.Value;
    if (cert is null || cnf is null) return Results.Unauthorized();

    var expected = JsonDocument.Parse(cnf).RootElement.GetProperty("x5t#S256").GetString();
    var actual = Base64Url.EncodeToString(cert.GetCertHash(HashAlgorithmName.SHA256));
    return expected == actual ? Results.Ok(await LoadAccountsAsync(user)) : Results.Unauthorized();
}).RequireAuthorization();

Certificate validation itself (chain to the CDR CA, revocation) must also be configured. This snippet only shows the token-to-certificate binding check.

Open Banking in Australia isn't just "an API with OAuth" 🔐

🪪 Every call uses mutual TLS with certificates from the CDR's own CA

🧷 Access tokens are bound to the client's certificate, so a stolen token is useless

📨 Authorisation requests are pushed and signed (PAR + PKCE)

🎛️ Consent has a lifecycle, and consumers can withdraw it from either side

Takeaway: design the consent store first. Everything else hangs off it.

#OpenBanking #CDR #FAPI #APISecurity

19.8 Regulation & resilience for architects Added

Banking rules turn into concrete design requirements: tolerance levels become RTO/RPO, service-provider risk becomes exit plans, and AML becomes monitoring and record keeping.

RegulationKey public factsWhat it means in a design
APRA CPS 234 Information SecurityIn force since 1 July 2019. Notify APRA within 72 hours of a material information security incidentAsset inventory, control testing, third-party security assurance, incident runbooks
APRA CPS 230 Operational Risk ManagementEffective 1 July 2025. Replaced CPS 231 (outsourcing), CPS 232 (business continuity) and equivalent super and health standards. Some parts for smaller entities, and legacy service-provider contracts (unless renewed earlier), had until 1 July 2026. Notify APRA within 24 hours of a disruption outside toleranceTolerance levels per critical operation, mapped dependencies, BCP and DR testing, material service provider register and exit plans
AML/CTF Act (AUSTRAC)The 2024 amendments changed obligations for existing reporting entities from 31 March 2026 and added new sectors from 1 July 2026. TTR and SMR stay the same until 2029. IFTI reporting continues until a move to new international value transfer reports on or after 31 March 2029Customer due diligence, real-time and batch monitoring, travel-rule data on transfers, reporting pipelines, long record retention
PCI DSS v4.0.1Industry standard enforced through card schemes and acquirers (not a law)Scope reduction, tokenisation, segmentation, MFA, script controls on payment pages
  • Turn a tolerance level into numbers: "payments down no more than X hours, lose no more than Y minutes of data, keep Z% throughput in degraded mode".
  • CPS 230 makes cloud and SaaS vendors "material service providers": design for exit (portable data, IaC, a tested plan B).
  • Logs are evidence. Make them complete, tamper-evident, retained for the required period and free of secrets and card numbers.
How to implement it
NeedLibrary / APINotes
Evidence & tracingOpenTelemetry.Extensions.Hosting + an exporter (e.g. Azure.Monitor.OpenTelemetry.AspNetCore)Trace every payment end to end; measure against tolerance levels
RedactionMicrosoft.Extensions.Compliance.RedactionKeep PANs, account numbers and personal data out of logs
RetentionAzure.Storage.Blobs with immutable (WORM) storage policiesTamper-proof audit and AML records for the retention period
Fraud / AML scoringMicrosoft.ML, or call a model service over HTTPMost banks use a dedicated monitoring platform; .NET usually calls it and handles the case workflow

APRA's CPS 230 in one sentence for architects 🏗️

"Know your critical operations, set limits on how badly they can fail, and prove you can stay within them."

⏱️ Tolerance levels → RTO, RPO and degraded-mode SLOs

🤝 Material service providers (yes, your cloud) → exit plans you have actually tested

🧪 Annual severe-but-plausible scenario testing → game days, not slide decks

Which of your systems would fail a "lose a region on payday" test?

#CPS230 #OperationalResilience #Banking #Architecture

19.9 Core banking & ledger basics Added

A ledger records every movement as balanced, immutable double entries. Balances are derived from them, and every posting must happen exactly once.

📒 Double entry

Every journal's debits equal its credits. Money is never created or lost, only moved between accounts.

🔒 Immutable

Never update or delete an entry. Fix mistakes with a reversing entry, so the history is the audit trail.

🔁 Idempotent postings

Each posting carries a unique key (e.g. the scheme's transaction ID). A unique index makes a replay harmless.

💵 Ledger vs available

Ledger = what has posted. Available = ledger − holds (+ any overdraft limit). Customers care about available.

🌙 End of day

Classic cores run batch jobs: interest accrual, fees, statements, GL feeds. Real-time rails need posting during those windows too.

⚡ Real-time challenges

Hot accounts (many postings per second), 24/7 availability, ordering, and matching holds to later clearing records.

  • Money is decimal (or integer cents) with a currency, never double.
  • Use optimistic concurrency on balances, or derive balances from entries, so two postings can't overwrite each other.
  • Reconcile daily: sum of entries = balances = what the rails and RITS settlement say.
How to implement it
NeedLibrary / APINotes
Money valuesbuilt-in decimal, Math.Round(x, 2, MidpointRounding.ToEven); or NodaMoneyBanker's rounding avoids upward bias over millions of postings. A small Money record is often enough
No double postingbuilt-in EF Core unique index on the idempotency keyInsert the journal and its key in one transaction; a duplicate fails on the index
Balance updatesbuilt-in EF Core concurrency token ([Timestamp] / IsRowVersion())Catch DbUpdateConcurrencyException, reload and retry
public readonly record struct Money(decimal Amount, string Currency)
{
    public Money Round() => this with { Amount = Math.Round(Amount, 2, MidpointRounding.ToEven) };
}

public async Task PostAsync(string idempotencyKey, Guid debitAcct, Guid creditAcct, Money amount)
{
    await using var tx = await db.Database.BeginTransactionAsync();
    var journal = new Journal { IdempotencyKey = idempotencyKey };   // unique index
    journal.Entries.Add(new Entry(debitAcct,  -amount.Amount, amount.Currency));
    journal.Entries.Add(new Entry(creditAcct, +amount.Amount, amount.Currency));
    db.Journals.Add(journal);

    var debit = await db.Accounts.SingleAsync(a => a.Id == debitAcct);
    debit.LedgerBalance -= amount.Amount;    // Account has a [Timestamp] row version
    var credit = await db.Accounts.SingleAsync(a => a.Id == creditAcct);
    credit.LedgerBalance += amount.Amount;

    try { await db.SaveChangesAsync(); await tx.CommitAsync(); }
    catch (DbUpdateException ex) when (IsUniqueViolation(ex)) { /* already posted: no-op */ }
}

The when filter only swallows unique-key violations (a replayed posting). A DbUpdateConcurrencyException (another posting changed the balance first) still bubbles up, so the caller can reload and retry with the same key.

Three ledger rules every payments engineer should tattoo somewhere 📒

1️⃣ Double entry: every debit has an equal credit

2️⃣ Immutable: never edit an entry, reverse it

3️⃣ Idempotent: the same posting key can only post once

💵 Bonus: "ledger balance" and "available balance" are different things (holds!)

Get these right and most "the balance is wrong" incidents disappear.

#CoreBanking #Ledger #Payments #SoftwareEngineering

⚡ Quick-fire Q&A

What's the difference between clearing and settlement?

Clearing is exchanging payment instructions and working out obligations. Settlement is the final movement of central-bank money between the banks' Exchange Settlement Accounts in RITS. NPP settles each payment in real time (FSS); BECS and cards settle net in batches.

Why can an NPP payment be released to the payee immediately?

Because the RBA's Fast Settlement Service settles each NPP payment individually and with finality, 24/7, before the funds are made available. There is no interbank credit risk to cover.

How would you migrate a biller from BECS direct debits to PayTo?

Notify customers (banks' terms require at least 30 days), create Migrated DDR Mandates in the MMS, run both rails in parallel, cancel the old BECS authorities, and handle real-time declines, pauses and cancellations in the billing flow.

A payment send to the scheme times out. Do you retry?

Not blindly. A timeout means "unknown". Query the status using the end-to-end ID or UETR, and only resend with the same idempotency key if the scheme confirms it never received it.

How does CPS 230 change your DR design?

You design to board-approved tolerance levels for each critical operation (maximum outage, maximum data loss, minimum service level), map every dependency including material service providers, and prove it with an annual severe-but-plausible scenario test, plus a tested exit plan for key vendors.

← 18 · Agentic AI