Why this matters
- Payment flows are chains of cause and effect: authorize → capture → settle; readers must not see settle before capture.
- Global serializability is expensive across ShardPay's 256 shards; causal consistency preserves what users actually care about.
- Session guarantees (read-your-writes) are a weak form of causal consistency every mobile banking app expects.
- Interviewers map happens-before to concrete API and messaging design, not just Lamport's paper.
The happens-before relation
Event A happens-before B (written A → B) if A could have influenced B. Same-thread order, lock unlock before lock acquire, message send before receive, and transitive chains all create → edges. If neither A → B nor B → A, events are concurrent.
ShardPay models transfer lifecycle as a DAG: TransferInitiated → FundsReserved → TransferCompleted → SettlementPosted. Any read path that serves merchant dashboards must not show SettlementPosted without all predecessors visible for that transferId.
Walkthrough: the out-of-order dashboard
Before causal guarantees, a merchant refreshed their dashboard and briefly saw "settled" on a transfer still showing "pending" in the detail drawer — different read replicas lagged differently. ShardPay attached a causal_token (vector summary) to each API response; subsequent reads pass the token so the load balancer routes to a replica at least as fresh as the token requires.
Key points
- Happens-before (→) — a partial order capturing cause and effect, not wall-clock time. ShardPay's saga orchestrator emits events only after predecessor steps commit so downstream consumers inherit a valid → chain per transfer.
- Causal consistency — every read observes a state that respects → for all events it sees. ShardPay merchants never see a refund before the original charge on the same transferId, even though unrelated transfers on other shards may appear in any order.
- Session guarantees — read-your-writes, monotonic reads, and monotonic writes are progressively stronger session properties. ShardPay's mobile API returns a session token after POST /transfers so the next GET from the same device reads a replica that includes that write.
- Linearizability vs causal — linearizability orders all operations as if on one global timeline; causal only orders related ones. ShardPay uses linearizable balance reads on the hot path (single shard) and causal ordering for cross-service event projections.
- CRDTs — conflict-free replicated data types merge concurrent updates without coordination. ShardPay uses CRDT-style
G-Counteronly for non-financial stats (page views on merchant portal), never for ledger balances.
Causal consistency in the payment ledger
Cross-shard transfers touch two account shards plus fraud and notification services. ShardPay does not serialize the entire cluster; it serializes per transferId. The orchestrator writes a causal_context blob (parent event IDs) into each step's outbox row. Projectors consume Kafka topics keyed by transferId so partition order preserves → within a transfer.
Unrelated transfers — Alice pays Bob, Carol pays Dave — remain concurrent. Settlement batching may interleave them arbitrarily; regulators care about per-transfer chains, not global total order.
Walkthrough: parent-funded child transfer
A parent account transfer funds a child account, which immediately pays a merchant. Three events: ParentDebit, ChildCredit, MerchantCredit. Causal consistency requires any observer of MerchantCredit to also observe ParentDebit and ChildCredit. ShardPay's notification service subscribes with causal_token chaining so push notifications arrive in sensible order.
// ShardPay causal context carried across saga steps (Java 17)
public record CausalContext(
String transferId,
List<String> predecessorEventIds,
long lamportTimestamp
) {
public CausalContext withNewEvent(String eventId, long lamport) {
var next = new ArrayList<>(predecessorEventIds);
next.add(eventId);
return new CausalContext(transferId, List.copyOf(next), lamport);
}
}
// Projector skips events whose predecessors are not yet applied
public void apply(SettlementPosted event, CausalContext ctx) {
if (!appliedEvents.containsAll(ctx.predecessorEventIds())) {
bufferUntilPredecessors(event, ctx);
return;
}
ledgerProjector.postSettlement(event);
appliedEvents.add(event.eventId());
}
Session tokens on the read path
Causal consistency for end users often reduces to session guarantees. After creating a transfer, the client must see it on refresh. ShardPay stores the write replica's commit_index in a signed cookie; read APIs prefer replicas at or past that index. This is cheaper than full vector clocks on every mobile request while fixing the "I paid but balance unchanged" support ticket class.
Stronger than session causal is global causal — all clients see the same relative order of causally related events. ShardPay enforces global causal only on compliance export pipelines, not on merchant dashboards where slight lag is acceptable.
Quick recall
Everything you need if you only revisit this box.
- Happens-before captures cause-effect; unrelated events across shards stay concurrent.
- Causal consistency preserves per-transfer chains cheaper than global linearizability.
- Session tokens and causal context blobs bridge theory to ShardPay's mobile and projector read paths.
Test yourself
Answer these before moving on — recall is what makes it stick.