Abstract
Financial systems frequently describe transactions using apparently simple states such as pending, completed, failed, or reversed. These labels suggest that every participant shares the same understanding of when an operation becomes final.
In distributed financial infrastructure, that assumption is usually false.
An internal ledger may consider a transaction complete once its accounting entries are committed. A custody service may consider it complete once a signature is produced. A blockchain adapter may consider it complete when the transaction enters the mempool, while another subsystem waits for multiple confirmations. A payment processor may report success before bank settlement, and a card transaction may remain economically reversible long after every internal service has marked it completed.
This article examines finality mismatch as an architectural problem. We explore how different subsystems define completion, how premature finality creates economic and operational risk, and how financial platforms can model irreversible, revocable, probabilistic, and externally observed outcomes without collapsing them into misleading status fields.
A transaction is not final because one service says it is done. It is final only relative to a clearly defined trust domain and reversal model.
The word “completed” is doing too much work
One of the most dangerous fields in financial infrastructure is often also one of the simplest:
status = completed
It looks harmless. The transaction is no longer pending, no error occurred, and some operation reached its expected endpoint.
The difficulty begins when someone asks what completed actually means.
Does it mean the ledger entries were committed?
Does it mean the funds were reserved?
Does it mean a cryptographic signature was produced?
Does it mean the transaction was submitted to an external network?
Does it mean the external network accepted it?
Does it mean settlement occurred?
Does it mean settlement can no longer be reversed?
Different services may answer these questions differently while using the same status value.
Nothing necessarily crashes. No schema constraint is violated. Every component may behave exactly as designed.
The system still becomes wrong because it uses one word to represent several distinct realities.
Finality mismatch is not merely poor naming. It is a disagreement about the meaning of state.
Finality is relative to a domain
A transaction can be final inside one subsystem while remaining uncertain in another.
Consider a withdrawal funded by an internal ledger and settled over a blockchain network.
The ledger can atomically commit the withdrawal reservation:
debit(customer_available_balance)
credit(withdrawal_pending_account)
From the ledger’s perspective, this transition is durable. The entries exist, the accounting invariant is preserved, and the same withdrawal cannot reserve the funds again.
That is ledger finality.
Custody may then validate the transaction and produce a threshold signature. Once the signing round succeeds, the authorization artifact exists and can be submitted to the network.
That is signing finality.
The blockchain adapter may broadcast the transaction and receive a transaction hash.
That is submission finality.
A node may include the transaction in a block.
That is inclusion finality.
The application may wait for an additional confirmation policy before treating the transaction as operationally settled.
That is confirmation finality.
Later, reconciliation may verify that the external movement and internal accounting state agree.
That is reconciliation finality.
These states are related, but they are not interchangeable.
The mistake is not having several forms of finality. Distributed financial systems inevitably have them.
The mistake is pretending they are one state.
Local correctness does not create global finality
A ledger transaction can be perfectly correct and still describe an economically incomplete operation.
Suppose the internal accounting entries are committed, but the external settlement fails permanently. The ledger did not violate its invariant. It correctly represented that funds moved from an available account into a pending withdrawal account.
What failed was not local accounting correctness. What failed was the assumption that an internal commit implied successful external completion.
The reverse can happen as well.
An external settlement may succeed, but the internal service may crash before persisting the confirmation. The blockchain or payment network now considers the movement complete, while the application still believes it is pending or failed.
Again, neither domain is necessarily corrupt.
The domains disagree because the observation linking them was lost.
Global finality therefore cannot be inferred from a single local commit. It must be derived from a sequence of evidence across trust boundaries.
Irreversible finality and operational finality are not the same
Financial systems often use the word irreversible too casually.
Some state transitions are internally immutable but externally reversible. Others are externally difficult to reverse but internally represented as pending. Some are probabilistically final. Some are legally contestable even when technically complete.
This distinction matters.
An append-only ledger entry may be immutable in the sense that it cannot be deleted or edited. But the financial effect can still be reversed using compensating entries.
A blockchain transaction may become increasingly difficult to reorganize as confirmations accumulate, but the system still chooses an operational threshold at which it is willing to treat the transaction as settled.
A card payment may be authorized and captured, yet remain exposed to chargeback or dispute mechanisms.
A bank transfer may appear completed in an API while remaining subject to later reconciliation, return, or settlement processing.
Finality must therefore be defined relative to the reversal mechanism.
A useful distinction is:
technical finality:
the recorded state transition cannot be silently rewritten
operational finality:
the system is willing to continue downstream processing
economic finality:
the value transfer is no longer expected to reverse
legal finality:
the transfer is considered binding under the relevant rules
These categories may converge eventually.
They do not necessarily converge at the same moment.
Probabilistic finality requires explicit risk policy
Blockchain settlement makes finality mismatch especially visible because many networks do not offer immediate absolute finality.
A transaction may be broadcast, observed in the mempool, included in a block, and then considered increasingly stable as additional blocks are produced.
The application chooses when to treat the transaction as sufficiently final for its own purposes.
That decision is not purely technical. It is a risk policy.
A low-value deposit may be credited after fewer confirmations because the economic exposure is limited. A high-value withdrawal may require stronger confirmation, additional monitoring, or delayed availability.
The blockchain does not decide the business threshold.
The platform does.
This means confirmation policy belongs in the decision record.
A system should be able to explain why a particular transaction was treated as final under a particular network state and risk policy.
For example:
SettlementDecision:
transaction_id: tx_9148
network: example_chain
block_height_observed: 18_442_981
inclusion_block: 18_442_976
confirmations: 5
required_confirmations: 5
policy_version: settlement_policy_v12
amount_class: medium
decision: operationally_final
Without this context, the system may know that it marked a transaction complete but not why that completion was justified.
That is not provenance. It is a timestamped opinion.
Premature finality creates downstream corruption
Marking a transaction final too early does not merely create an inaccurate status.
It permits downstream actions that may be impossible to unwind cleanly.
A deposit credited before sufficient settlement confidence may be used to fund another withdrawal. A payment marked settled may release goods or services. A custody workflow may authorize a subsequent transaction based on funds that remain externally uncertain. A reconciliation process may exclude records that the system has already classified as complete.
The original premature decision propagates.
This is what makes finality mismatch systemic.
A status field becomes a precondition for another service. That service trusts the meaning of the status. Another service trusts the second service’s action.
Eventually, an uncertain observation becomes embedded in several layers of supposedly final state.
The system has converted ambiguity into confidence without acquiring additional evidence.
Financial systems are remarkably talented at turning one incorrect boolean into an organizational event.
Finality must be monotonic, but not simplistic
A robust transaction model should move through increasingly strong states as evidence accumulates.
That progression should be monotonic in meaning. A transaction should not move casually from confirmed back to broadcast simply because one service received an older event.
However, monotonic progression does not mean every transition is irreversible.
The system may need explicit states such as reversed, expired, rejected, or reconciliation_required.
The important property is that these are domain transitions, not silent rewrites.
A simplified model might look like this:
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettlementState {
Created,
InternallyReserved,
Authorized,
Signed,
Submitted,
Included,
OperationallyFinal,
ReconciliationVerified,
Reversed,
Failed,
}
impl SettlementState {
pub fn can_transition_to(self, next: SettlementState) -> bool {
use SettlementState::*;
matches!(
(self, next),
(Created, InternallyReserved)
| (InternallyReserved, Authorized)
| (Authorized, Signed)
| (Signed, Submitted)
| (Submitted, Included)
| (Included, OperationallyFinal)
| (OperationallyFinal, ReconciliationVerified)
| (InternallyReserved, Failed)
| (Authorized, Failed)
| (Signed, Failed)
| (Submitted, Failed)
| (Included, Reversed)
| (OperationallyFinal, Reversed)
)
}
}
This is intentionally simplified, but it shows the key idea.
The state machine distinguishes evidence acquisition from business interpretation. A transaction being submitted is not the same as being included. Inclusion is not the same as operational finality. Operational finality is not the same as reconciliation verification.
The system stops asking whether the transaction is done.
It asks which claims about the transaction are currently justified.
Evidence should drive state transitions
Many systems update state because a service emitted an event.
That is necessary, but insufficient.
The event should represent evidence, not merely opinion.
For example, TransactionSubmitted may include the network transaction identifier and submission response. TransactionIncluded should include the observed block reference. OperationalFinalityReached should record the policy and evidence that justified the transition.
A durable event might look like:
{
"event_type": "OperationalFinalityReached",
"transaction_id": "tx_9148",
"external_reference": "0xabc123",
"observed_at": "2026-07-13T15:42:09Z",
"evidence": {
"inclusion_height": 18442976,
"observed_height": 18442981,
"confirmations": 5
},
"policy": {
"policy_id": "settlement_policy_v12",
"required_confirmations": 5
}
}
This matters because finality is a conclusion derived from evidence.
If the system records only the conclusion, it cannot later verify whether that conclusion was reasonable.
Do not use one status field for several domains
A single transaction status often becomes overloaded because it is convenient for APIs and user interfaces.
The UI wants one answer.
Pending, completed, or failed.
The domain does not owe the UI a simplified ontology.
A better design separates internal state from presentation state.
Internally, the system preserves the detailed state machine. Externally, an API projection can map several states into a user-facing category.
For example:
Created, InternallyReserved, Authorized, Signed
-> processing
Submitted, Included
-> awaiting_confirmation
OperationallyFinal
-> completed
ReconciliationVerified
-> completed_verified
Reversed
-> reversed
Failed
-> failed
The user interface receives something understandable, while the platform preserves the distinctions needed for correctness, operations, and recovery.
Collapsing internal semantics for frontend convenience is a very efficient way to make production incidents harder to explain later.
Cross-system finality requires correlation
A transaction crossing several systems needs a stable identity across all of them.
The internal ledger entry, custody request, network transaction, provider reference, settlement record, and reconciliation result must be correlated.
Without this, the platform may observe all the necessary evidence but fail to understand that the records belong to the same economic operation.
A transaction identity model might include:
business_operation_id
ledger_transaction_id
custody_request_id
external_transaction_id
provider_reference
reconciliation_record_id
These identifiers should not replace one another.
They represent different domains.
The business operation is not identical to the external transaction. A retry may produce a new external transaction while remaining part of the same business intent. A reconciliation record may compare several external observations against one internal operation.
Correct correlation allows the system to answer a more important question than “what is the status?”
It allows the system to explain which stages of the same economic intent have reached finality.
Retries complicate finality
Retry behavior can create several technical attempts for one business operation.
A blockchain transaction may be replaced. A provider request may time out and be resubmitted. A settlement adapter may create a new transport-level request while preserving the same idempotency key.
If the system confuses attempt identity with operation identity, finality becomes ambiguous.
One attempt may fail while another succeeds.
The business operation should not be marked failed merely because one transport attempt failed. Likewise, it should not be considered successful twice because two acknowledgments were received.
This requires separating intent from execution attempts:
PaymentIntent:
id: payment_771
desired_outcome: transfer 500 USDC to recipient X
ExecutionAttempt 1:
id: attempt_1
outcome: timeout_unknown
ExecutionAttempt 2:
id: attempt_2
outcome: externally_confirmed
The finality of the business operation is derived from the combined evidence of its attempts.
This is one reason API-layer idempotency alone is not enough. Finality lives deeper than the request boundary.
Unknown is a legitimate state
Systems resist representing uncertainty because product flows prefer definitive answers.
But unknown is often the most honest state available.
A request timed out after submission. The external provider cannot yet confirm whether it processed the operation. The network transaction is visible on one node but not another. The service crashed after sending a request but before persisting the response.
The transaction is not safely failed.
It is not safely successful.
It is unknown.
This state should trigger observation, reconciliation, or bounded retry logic. It should not be forced into failure simply because an API contract expects a binary result.
A system that cannot represent uncertainty will misclassify it.
Misclassified uncertainty is one of the main causes of duplicate execution and unsafe compensation.
Compensation depends on finality semantics
Compensation is only safe when the system understands which effects actually occurred.
If an internal reservation exists but no external settlement was submitted, releasing the reservation may be straightforward.
If settlement was submitted but its outcome is unknown, releasing the reservation may allow the customer to spend funds that could still leave externally.
If external settlement succeeded, the correct response may be to finalize the internal ledger rather than reverse anything.
The same visible timeout can require three different actions depending on the underlying finality state.
This is why retry and compensation logic must depend on evidence-backed state, not only on error type.
A timeout describes an observation failure.
It does not describe the transaction outcome.
Reconciliation provides a stronger form of finality
Operational systems often need to act before every external source has converged.
That is reasonable.
But later reconciliation should strengthen confidence in the result.
A transaction considered operationally final after blockchain confirmation may later become reconciliation verified after comparing internal ledger state, external transaction state, fees, and resulting balances.
This is a stronger claim.
The transaction is no longer merely believed to have settled. The system has verified that its internal representation agrees with external evidence.
Reconciliation finality is especially important for reporting, accounting, regulatory evidence, and incident closure.
It should not be confused with the earlier operational decision that allowed the platform to continue processing.
Observability must preserve finality transitions
A transaction trace should show not only service calls but semantic progression.
Operators need to know when the transaction was reserved, signed, submitted, included, treated as final, reconciled, or reversed.
This timeline should be reconstructible from durable events.
For example:
14:01:02 WithdrawalCreated
14:01:02 FundsReserved
14:01:04 ComplianceApproved
14:01:06 CustodyAuthorized
14:01:09 SignatureProduced
14:01:11 TransactionSubmitted
14:02:03 TransactionIncluded
14:06:45 OperationalFinalityReached
02:00:11 ReconciliationVerified
This is far more useful than a database row showing status = completed.
The row tells you where the system ended.
The timeline explains how it arrived there.
Finality policy is part of risk architecture
Different products, networks, providers, amounts, and customer contexts may justify different finality policies.
A platform should not scatter these decisions across services as hard-coded thresholds.
Finality policy should be explicit, versioned, observable, and associated with the transaction decision.
For example, the required evidence for a low-value retail deposit may differ from a high-value institutional transfer. A network with deterministic finality may be treated differently from one with probabilistic settlement. A provider with delayed reversal risk may require a longer economic finality window.
These are not implementation details.
They define when the system is willing to expose value, release goods, update customer balances, or permit subsequent transactions.
Finality policy is therefore part of economic risk management.
Conclusion
Distributed financial systems do not have a single universal moment of completion.
Ledger commit, custody authorization, transaction submission, external inclusion, confirmation, economic settlement, and reconciliation represent different forms of finality across different trust domains.
Collapsing those states into a generic completed flag creates semantic ambiguity, unsafe downstream behavior, and difficult recovery.
A resilient system models finality explicitly. It distinguishes business intent from execution attempts, conclusions from evidence, operational completion from reconciliation verification, and internal durability from external settlement.
The right question is not:
“Is the transaction completed?”
The right question is:
“Which claims about this transaction are now justified, by which evidence, under which policy, and within which domain?”
Until the system can answer that precisely, completed is not a state.
It is an assumption.
Top comments (7)
This is a really precise taxonomy. The distinction between evidence acquisition and business interpretation in your state machine is the part most teams skip — they collapse it into a single
statuscolumn and then spend years adding boolean flags to reconstruct what they threw away.The PSD2 / open banking API domain runs into this exact mismatch in a form that catches people off guard: a bank transaction has its own two-stage finality model (
pending→booked), and "booked" at the bank is neither operational finality nor economic finality in your framework. It's closer to inclusion finality — the bank has committed the accounting entry, but the transaction can still be returned (R-messages in SEPA), reversed (card chargebacks), or corrected (bookingDate adjustments where the bank overwrites the entry retroactively).A few patterns that fall out of treating bank-sourced transactions through your lens:
Pending as a separate trust domain, not a status. When you ingest from a bank API,
pendingtransactions don't have a transactionId yet — they only have a temporary authorization reference. If you key your internal dedup ontransactionId, the pending entry and the later booked entry with the same amount and date become two separate records, because the bank's pending→booked transition is a domain transition (your word), not a status update. The fix is to treat the absence of a stable external ID as its own evidence state, not as missing data.bookingDate vs valueDate as two different finality clocks. The booked transaction has
bookingDate(when the bank committed it) andvalueDate(when it takes economic effect for interest calculations). In your taxonomy, these map to technical finality and economic finality respectively — and they can be days apart for FX transactions (T+2 settlement). Systems that usebookingDatefor balance calculations are silently treating technical finality as economic finality.The reversal problem and monotonic state machines. Your
Reversedstate is well-placed. In bank data, a reversal doesn't delete the original — it arrives as a new transaction with a negative amount and a reference back to the original entry. If your state machine doesn't have aReversed/Compensatedterminal state, you'll either double-count (original + reversal both present asOperationallyFinal) or silently lose the reversal if you try to mark the original asFailed(which violates monotonicity). The ISO 20022 camt.054 bank-to-customer debit notification models this cleanly: the original entry stays immutable, the reversal is a new entry linked byoriginal_transaction_reference.The practical implication of your
SettlementDecisionrecord pattern: for bank API integrations, you want to persist not justbooked=truebut(bank_transaction_id, booking_date, value_date, transaction_status, observed_at, api_cursor_position). Theobserved_at+ cursor pair is your evidence chain — it's what lets you reconstruct when you learned the transaction became booked, which is different from when the bank booked it. That gap is where reconciliation exceptions live, and it's also where regulators look during audits.John, this is an excellent extension of the model.
The pending-to-booked transition is a particularly good example of why evidence acquisition and business interpretation must remain separate. What the bank exposes as a lifecycle transition is not just a status mutation inside the same object. In many cases, it is a change in the quality and identity of the evidence itself.
That temporary authorization reference is effectively saying: “we have observed intent or provisional execution, but we do not yet have a stable external identity for the accounting event.” Treating the missing transaction ID as incomplete data loses that meaning. It is its own evidence state.
I also really like the bookingDate versus valueDate distinction. It suggests that bank integrations should often model at least three clocks rather than one:
the time the economic event becomes effective,
the time the source institution commits or reports it,
and the time our system actually observes it.
Those clocks can diverge significantly, and each answers a different question. Most reconciliation problems are hidden inside that distance.
Your reversal example is exactly why I prefer append-only correction semantics. A reversal is not proof that the original event never happened. It is a new economic event that changes the net effect of the original one. Rewriting the original record into Failed destroys history and collapses two valid events into one misleading state.
The ISO 20022 reference is useful here because it makes the relationship explicit: immutable original entry, compensating entry, durable linkage between them. That is much closer to ledger semantics than the common “update the status and hope reporting understands what happened” model.
The ingestion provenance point may be the most operationally important one. Persisting only
booked=truerecords the conclusion but throws away the evidence path. The combination of source transaction identity, booking and value dates, observation time, and cursor position gives you enough information to distinguish source-time behavior from ingestion-time behavior.That distinction matters because the reconciliation gap is often not between internal truth and bank truth. It is between when the bank knew something and when we were able to observe it.
Really appreciate this comment. It adds a concrete open-banking implementation model to the finality taxonomy and makes the evidence chain much sharper.
The three-clocks framing is the part I keep coming back to. Most reconciliation bugs I've seen collapse exactly the distance you're describing — a system records one timestamp, calls it "the transaction time," and then every downstream consumer has to guess which of the three clocks it actually measured.
Your split — economic-effective, institution-commit, and system-observed — is sharper than the usual booked/value distinction because it makes the provenance explicit rather than implied. And once provenance is explicit, the cursor-position point follows naturally: the cursor isn't just "where did I get to," it's "what was the latest observation I could have made," which is the thing you need to reason about gaps and dedup across replays.
One thing worth naming directly: the reconciliation gap you point at — between when the bank knew something and when we were able to observe it — is also where the cert/eIDAS cost shows up for EU integrations in particular. The bank's commit clock and your observation clock are separated by consent windows, SCA reauth round-trips, and rate limits, none of which are under your control. Persisting the observation time alongside the booking/value dates is what lets you later distinguish "the bank was slow" from "we were slow," and only one of those is actionable on your side.
The append-only correction model maps cleanly onto ISO 20022's immutable-original + compensating-entry, as you say, and I'd push it one step: the compensating entry should carry a back-reference to the original and a reason code, not just a net effect. That's what turns a reversal from "the number changed" into "an auditable economic event with a cause" — which matters most when the correction crosses a consent boundary (e.g. a transaction booked under one consent and reversed after reauth, where the linkage is the only thing that keeps the audit trail honest).
Appreciate the extension — the evidence-state framing for the pending→booked transition is the piece I'd underweighted.
The taxonomy only survives contact with real integrations if reversibility rides on the wire. If a settlement record just carries status=completed, the next team that consumes it re-collapses your six finalities back into one boolean and the mismatch comes straight back. What has worked better for me is shipping the reversibility facts alongside the state itself: a reversal class, the window it stays reversible until, and which party is allowed to reverse it. The producer then stops asserting global finality and the consumer derives its own threshold from those fields.
The case your taxonomy points at but doesn't fully chase is a reversal that lands after a downstream service already acted on operational finality, after goods shipped or dependent funds got released. A cleaner status enum does nothing there. What helps is attaching a compensation obligation to the receipt, so the record names its own undo path and says which party carries reversal liability during the window. Finality is really twinned with who absorbs the reversal, and that assignment has to be settled up front rather than discovered during an incident.
Smaller point on the probabilistic side: confirmation depth behaves better when it scales to value at risk instead of a global constant, since a few-cent movement and a large transfer have no reason to wait the same number of blocks. And whatever depth you pick, the reorg-then-resubmit path needs a stable idempotency key, or reconciliation will happily count the same movement twice.
This is a very strong extension of the model.
I completely agree that reversibility has to travel with the event. If the producer emits only
status=completed, every downstream consumer is forced to invent its own interpretation of what completed means. At that point, the original finality model is already lost.Carrying the reversal class, reversal window, authorized reversing party, and applicable liability makes the contract much more honest. The producer is no longer claiming universal finality. It is reporting the evidence and legal or operational constraints it actually knows, while each consumer decides whether that evidence is sufficient for its own risk boundary.
Your point about compensation obligations is especially important. Once a downstream system acts on operational finality, the question is no longer only whether the original transaction can be reversed. The real question becomes who absorbs the consequences of that reversal.
If goods have shipped, credit has been extended, or dependent funds have already been released, a later reversal creates a new obligation. A better state model does not solve that on its own. The receipt needs to carry the expected compensation path, the liable party, and the conditions under which that liability moves.
That makes finality inseparable from exposure allocation.
I would model the settlement receipt as something closer to this:
Then the consumer can derive its own operational threshold without pretending the underlying event has become globally irreversible.
I also agree on value-sensitive confirmation policy. A global confirmation constant is easy to implement but economically crude. Confirmation depth should be derived from value at risk, network characteristics, current reorg risk, and the cost of delayed availability.
And the stable idempotency key across reorg and resubmission is essential. The transaction hash identifies one network attempt. It does not necessarily identify the underlying economic intent. If reconciliation keys only on the hash, a replaced or resubmitted transaction can easily appear as a second movement.
So the stable identity has to live above the chain attempt:
That separation is what lets the system survive reorgs, replacement transactions, and retries without turning one movement into two accounting events.
Really good point. Finality is not only about when an event becomes stable. It is also about who carries the exposure while it remains reversible.
The schema reads right, and reversal_authority is the field that decides whether the rest is load-bearing. As written the producer reports all of it about itself, including who may reverse and who holds the liability. Have reversal_authority and liability_holder each sign their own entry and the receipt stops being one party's narration. A signed liability_holder can't quietly disown the exposure later. reversible_until turns into a dated promise with an author attached instead of a value that showed up on the wire.
Where it pays off is the reversal. A consumer that acted on operational finality and got caught can point to who committed to what, keyed to business_operation_id, before anything unwound. Exposure gets attributed from signatures already on record instead of reconstructed once the argument starts. Unsigned, the same fields are a clean description nobody is bound to.
In an append-only, evidence-backed ledger I've run, the dangerous part of this design is the mutable SettlementState plus the can_transition_to guard. Out-of-order observations and replayed events turn that guard into a matrix of pairwise exceptions, and one missed edge is enough to advance the record incorrectly. A sturdier shape is to store only the evidence set and derive the readable state by folding over it. Duplicates collapse naturally; late observations get re-evaluated in context, with monotonicity coming from the reduction rather than another branch in can_transition_to. The other trap is treating evidence as authoritative when it came from a single observer. An OperationalFinalityReached event should carry the observer's attributable identity alongside confirmations and policy version, because one RPC node can equivocate or eclipse two clients into different views. Otherwise "final" is exactly the timestamped opinion you warned about, just with better JSON.