← Work

Multi-Tenant Financial Aggregation Engine

Principal Engineer · Financial Services (NDA) · June 2021 – Nov 2023

This is a three person team project I originated. We built a distributed, multi-tenant financial data platform that aggregates real-time transaction streams from multiple banking endpoints into a single unified record per user. Banks expose data differently; varying APIs, formats, and schemas - making consolidation across institutions technically messy. The harder problem was multi-currency: a single user could hold accounts across multiple currencies, and producing one coherent view required a normalisation layer that did not exist off the shelf.

The platform processed 2.5 billion transactions monthly for 1M+ daily active users at a 10:1 write-to-read ratio, peaking at 10k TPS. Acquired and absorbed into the product pipeline of a regional financial institution in June 2023, with the multi-currency normalisation layer taken in as a whitelisted feature. We spent the following five months on the handover.

Multi-currency normalisation

Aggregating transaction inputs across multiple banking endpoints surfaced a data-marshalling problem early: incompatible field names, non-standard schemas, and a mix of API styles - REST, RPC, file feeds, and messaging queues - sometimes varying within the same institution. A dedicated Java marshalling and unmarshalling service using JAXB sat at the top of the pipeline and normalised every incoming feed into a single agreed schema before anything else touched it. This was the feature the acquirer was most interested in.

Pub/sub decoupling

Proposed by Silvester. The idea was to put a message queue between the marshalling layer and the internal pipeline, so that bank API instability had no path to the accounting logic. A delayed or failed feed would stay in the queue rather than surfacing as a processing failure downstream. We went with Kafka over RabbitMQ given the write volume and the value of replay capability.

Storage split

Cassandra/Scylla for the write path, built for this workload, scales horizontally, handles 10k TPS without significant operational overhead. PostgreSQL on the read side for anything needing transactional consistency, budget and category queries specifically. Redis in front for session data and aggregate reads where latency mattered.

Deduplication

Bank webhooks send duplicates. Rather than deduplicating at ingestion, which adds latency to the write path, we wrote blindly and handled deduplication downstream with a LIFO queue ordering on composite keys (user_id, amount, denomination). Idempotency keys on the aggregator handled anything that slipped through at the processing stage.

Architecture and design decisions

For anyone interested in the design thinking behind the system, these are the working documents from that process from the competing proposals, the trade-offs, to the reasoning that shaped what we built.
Fair warning: these are four years old (as of this writing in 2026); the grammar is rough in places, and they include several Loom links featuring my feedback to the team.

First round feedback Initial design proposals with written responses on data persistence, service communication, and the read/write split
Second round feedback Second iteration covering storage limitations and the case for a pub/sub model over synchronous RPC calls.
Guiding principles The document I wrote to set the engineering cadence for the team. Conway's Law, the argument for building the team structure before touching the architecture, and the principles we used to keep services from becoming a distributed monolith.
Financial aggregation platform architecture Three-layer architecture diagram showing ingestion boundary, processing core, and read and notification layer. Ingestion boundary RPC feed Y Bank Messaging Z Bank File feed G Bank REST API A Bank JAXB marshalling Normalises every feed into agreed schema Range monitor Flags silent feeds agreed schema Kafka Queue and replay Processing core Real-time aggregator Exactly-once via idempotency Dedup queue LIFO composite key Append DB WAL blind write Cassandra / Scylla 10k TPS write path Read and notification layer PostgreSQL Budget and categories Redis Session and aggregates Budget service Limit tracking Notification service Budget breach alerts SMS alerts Email alerts

Key decisions

Decision Detail
Architecture Event driven microservices Decoupled ingestion, transaction processing, and budget tracking. Failures in one service had no path to the others.
Multi-currency normalisation JAXB marshalling service Banks send data in too many formats to handle inline. JAXB converted every upstream feed into a single agreed schema before it entered the pipeline.
Messaging Kafka Decoupled the pipeline from bank SLA variability. Feeds that went slow or dropped stayed in the queue. Replay capability mattered too.
Write storage Cassandra / Scylla Write-heavy workload at 10k TPS. Horizontal scaling without significant operational overhead.
Read storage PostgreSQL and Redis PostgreSQL for transactional consistency on budget and category queries. Redis for sub-millisecond reads on session and aggregate data.
Exactly once processing Idempotency keys on aggregator At 10k TPS with async ingestion, duplicate events were a given. Idempotency keys on the aggregator prevented double counting.
Write path Append DB and LIFO dedup queue Blind WAL writes. Duplicates resolved downstream using composite key ordering on user_id, amount, and denomination.