DEV Community

Cover image for Finality Mismatch in Distributed Financial Systems: When “Completed” Means Different Things
Mayckon Giovani
Mayckon Giovani

Posted on

Finality Mismatch in Distributed Financial Systems: When “Completed” Means Different Things

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 (2)

Collapse
 
johnfrandsen profile image
John Frandsen

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 status column 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 (pendingbooked), 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, pending transactions don't have a transactionId yet — they only have a temporary authorization reference. If you key your internal dedup on transactionId, 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) and valueDate (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 use bookingDate for balance calculations are silently treating technical finality as economic finality.

The reversal problem and monotonic state machines. Your Reversed state 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 a Reversed / Compensated terminal state, you'll either double-count (original + reversal both present as OperationallyFinal) or silently lose the reversal if you try to mark the original as Failed (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 by original_transaction_reference.

The practical implication of your SettlementDecision record pattern: for bank API integrations, you want to persist not just booked=true but (bank_transaction_id, booking_date, value_date, transaction_status, observed_at, api_cursor_position). The observed_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.

Collapse
 
anp2network profile image
ANP2 Network

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.