Financial data becomes dangerous when a convenient story replaces the actual lifecycle.
A debit appears near the due date and has the same amount as a commitment, so the application calls it a payment. A later inflow has the same description, so it is labelled a reversal. A retry reuses the original transaction identity because the user intended one action.
Those guesses may look correct in a demonstration. They become liabilities when money is disputed, partially reversed, retried, delayed, or settled out of order.
A financial event contract should preserve what the source system knows, including the uncomfortable parts.
Identity has more than one level
There is usually an intent, an attempt, and a resulting transaction. They should not be collapsed into one identifier.
Consider a recurring commitment:
textcommitment -> mandate or collection instruction -> first transaction attempt -> retry transaction attempt -> settlement -> possible reversal transaction
The commitment identity stays stable across the series. Each attempt receives its own immutable transaction identity. A retry is a new event, even when it represents the same business intent.
This lets the system answer two different questions:
- Which transactions belong to this commitment?
- What happened to this specific attempt?
Reusing one identifier makes both answers ambiguous.
Status changes do not create a new transaction
A transaction can move through initiated, pending, settled, failed, or reversed states. A status update to the same attempt should retain the same source identity.
Creating a new transaction every time the status changes inflates history and makes idempotent synchronisation difficult. The consumer cannot tell whether it received a new financial event or a new observation of the old one.
I prefer:
textimmutable source transaction id + current lifecycle status + initiated timestamp + optional final settlement timestamp + source observation version or update time
The initiated time exists from the beginning. The settlement time remains empty until the transaction actually reaches final settlement.
A reversal is its own transaction
A reversal is not a boolean on the original debit. It is a new linked financial event, often an inflow, with its own identity, timestamps, amount, and lifecycle.
The original transaction can reference the reversal, and the reversal should identify what it reverses. The relationship may be one-to-many because partial or repeated reversals exist.
json{ "sourceId": "txn-original", "status": "REVERSED", "reversedBy": ["txn-reversal-1", "txn-reversal-2"] }
json{ "sourceId": "txn-reversal-1", "reversalOf": "txn-original", "direction": "INFLOW" }
The exact field names matter less than the explicit relationship.
Similarity is not authority
Matching a transaction to a commitment by title, amount, and date is tempting. It is also unsafe.
Two subscriptions can have the same amount. A merchant descriptor can change. A payment can arrive late. A partial payment can be followed by another. A reversal can share the original description. A user can make an unrelated purchase for the same value.
If the source system does not provide an authoritative commitment or mandate reference, the honest state is unlinked.
An application may offer a user-confirmed suggestion, but it should not present fuzzy matching as historical fact.
Ordering and pagination are part of the contract
Financial consumers often reconstruct state from pages of events. An API that does not specify deterministic ordering can return duplicates or omissions as new records arrive between requests.
The contract should define:
- The primary sort field.
- A stable tie-breaker.
- Cursor behaviour.
- Whether updates can move an item between pages.
- Tombstone or deletion behaviour.
- The retention window.
An example ordering might be settlement time descending, then immutable source identity descending. The choice depends on the domain, but the choice must be documented.
Version the vocabulary
Transaction types and lifecycle states evolve. Consumers should not discover a new enum value only after deserialisation fails in production.
Versioned schemas, documented unknown-value behaviour, and representative examples make the relationship between producer and consumer explicit.
I also want error semantics that distinguish invalid requests, unavailable dependencies, stale cursors, and retryable failures. "Something went wrong" is not enough for a financial synchronisation loop.
Keep facts separate from interpretations
The source contract should carry authoritative facts: identities, amounts, currencies, directions, statuses, links, and timestamps.
Higher-level systems can interpret those facts into commitment coverage, cash-flow analysis, or forecasts. The interpretation should reference its source evidence and version.
This separation makes corrections possible. If an event is later reversed, the analysis can be recomputed without rewriting what the source originally reported.
A good financial event contract does not force the data into the cleanest story. It preserves enough identity and lifecycle detail for the truth to remain reconstructable when the story becomes complicated.