A payment request spans a dozen microservices — where does the timeout actually sit? Sampling strategy, TraceID propagation rules and a real incident walkthrough for finance.
A core banking system makes far stricter observability demands than ordinary business software: a single request crosses a dozen microservices spanning accounts, clearing, risk control and channels; regulators require key transactions to be auditable and replayable; and every second of an outage costs real money. The generic “probabilistic sampling plus digging through logs afterwards” approach does not hold up here — when a transfer fails, you need the whole story of that one transaction, not a statistically representative slice.
Collecting every trace in full is too expensive, yet a financial system must guarantee that no critical transaction is ever missed. Observe supports rule-based sampling:
sampling:
- name: critical-tx
match: attributes["tx_type"] in ("transfer", "settlement")
rate: 1.0
- name: default
rate: 0.1
This keeps every transaction that regulators care about fully recorded while holding storage costs within an acceptable range. The distinction between “must never lose” and “good to have” is made explicit in config, not left to a global probability dial. When an auditor asks for the trace of a specific settlement three months later, the answer is a lookup, not an apology. Match on business attributes rather than technical ones — tx_type is stable and meaningful, whereas a service name changes the moment a team refactors.
Finance systems commonly suffer from “half traces”: the frontend-to-gateway span exists, but the trace breaks right after the gateway. The root cause is middleware that fails to propagate trace context. Write propagation into your onboarding spec:
traceparent / X-B3-TraceId header;Enforce these rules in code review and CI — relying on good intentions will always leave gaps. A single shared library that wraps HTTP and MQ clients with context propagation removes most of the burden, and a periodic scan can flag services that drop the header. For event-driven flows that span minutes or hours, propagate the trace id through the message payload or a dedicated header so the whole saga stays connected across queue hops.
Much of a financial settlement is not a synchronous call chain at all — it is messages flowing through Kafka or RocketMQ, with consumer lag, retries and dead-letter queues along the way. These flows break naive tracing because there is no single HTTP request to hang a trace on. The workable pattern is to treat the message as the unit of identity: stamp the trace id and a span context into the message header when it is produced, open a new span on the consumer side, and link it back to the producer span. The result is a single trace that runs from order placement through clearing and settlement across queue boundaries, so a stuck settlement becomes as easy to locate as a slow HTTP call. Without this, the most expensive failures in a payments system — the ones that sit in a queue silently — are precisely the ones you will never see.
At 14:03 one day, payment success rate dropped from 99.7% to 96% and an alert fired. The on-call engineer filtered traces by “payment failure” in Observe, sorted by latency, and saw the timeouts concentrated in the hop where settlement-service calls ledger-service:
ledger-service's DB query climbing from 8ms to 900ms;From alert to root cause took six minutes. Afterwards the team added an alert rule for that slow query, so the same problem now triggers automatically next time — the fix for an incident is never complete until the monitoring that would have caught it exists.
The goal of observability in finance is not a pretty dashboard. It is being able to say, on the next incident, which hop is stuck within a minute.