When a fast-growing fintech crossed the 10,000-user mark, a single PostgreSQL cluster handled everything: accounts, transactions, fraud scores, analytics. By the time it approached 10 million users, that same cluster had become a liability. The fix was not a bigger database, but a deliberate shift to polyglot persistence — using the right data store for each workload, all while keeping financial transactions consistent. This case study walks through the architectural decisions that kept the books balanced while letting the platform scale.
Why a Single Database Stopped Working at Scale
At 10K users, a relational database is a comfortable default. Joins are cheap, ACID guarantees are automatic, and the team can move quickly. At 10M users, the workload fractures into very different shapes:
- Write-heavy ledgers that demand strict consistency
- Read-heavy user profiles with flexible schemas
- Time-series event streams for fraud detection and audit trails
- Document-shaped product catalogs that change weekly
Trying to model all four with one engine meant over-indexing for some workloads and under-serving others. The team’s first instinct was vertical scaling, but the cost curve bent sharply upward past a few terabytes of hot data. The real unlock came from recognizing that different problems deserve different tools — a pattern known as hybrid data architecture.
The Core Principle: Match the Data Store to the Transaction Boundary
Not every workflow needs the same guarantees. The fintech mapped each business capability to a consistency tier:
- Strong consistency (SQL): Account balances, transfers, settlement, KYC status
- Eventual consistency (NoSQL): User preferences, notification history, marketing profiles
- Append-only streams: Audit logs, fraud signals, webhook deliveries
- Search-optimized stores: Merchant discovery, transaction search, support lookups
This mapping is the heart of any polyglot persistence strategy. Once the team drew the lines, the engineering work became much clearer: which transactions must stay inside one database, and which can safely cross system boundaries?
Choosing Stores for Each Workload
Rather than chasing novelty, the team picked boring, battle-tested engines that matched the access patterns:
PostgreSQL for Money Movement
The ledger stayed on PostgreSQL with synchronous replication and a primary-secondary topology per region. Money is unforgiving: a double-spend is a regulatory incident, not a glitch. Row-level locking, serializable isolation, and idempotent transaction keys handled the hard part. Sharding arrived later, but only along a single dimension — user ID — which kept cross-shard joins out of the critical path.
MongoDB for Customer Profiles
User profiles evolved constantly: new KYC fields, new marketing preferences, new compliance tags. MongoDB’s flexible schema let product teams ship changes without coordinated schema migrations. Reads were routed through a read-mostly cluster, and eventual consistency was acceptable because the profile never authorized a payment on its own.
Cassandra for the Activity Stream
Every payment, login, and device change generated an immutable event. Cassandra’s append-only model and tunable consistency handled billions of rows with predictable latency. The data was modeled by query, not by entity: one table per access pattern, with denormalization baked in.
Elasticsearch for Search
Merchants, transactions, and support tickets all needed fuzzy search and aggregations. Elasticsearch absorbed those workloads and was fed by change-data-capture streams from the systems of record, keeping search indexes near-real-time without becoming a source of truth.
Keeping Transactions Safe Across Systems
Mixing data stores introduces a classic distributed-systems headache: a transaction that touches multiple databases cannot rely on a single ACID engine. The team adopted three patterns to keep things honest.
The Saga Pattern for Cross-Store Workflows
For flows that touched more than one store — like a transfer that updated the ledger, recorded an event, and sent a notification — the team used the saga pattern. Each step had a compensating action. If the notification service failed, the saga replayed the relevant steps rather than rolling back the SQL transaction. Money was never lost because the ledger commit was the only irreversible step.
Change-Data-Capture as the Source of Truth for Projections
PostgreSQL’s write-ahead log was streamed into Kafka using logical decoding. Downstream consumers built their own projections: fraud scoring rebuilt its feature store from the stream, Elasticsearch indexed asynchronously, and data warehouse jobs ran on near-real-time CDC events. This kept the ledger authoritative while letting other stores stay loosely coupled.
Idempotency Keys Everywhere
Every write that crossed a system boundary carried an idempotency key. If a saga retried, or a CDC consumer reprocessed an event, the receiving service deduplicated safely. This turned transient failures into harmless no-ops instead of corrupted balances.
Observability and the Cost of Mixing Engines
Polyglot persistence is not free. Every new engine brought a new failure mode, a new monitoring stack, and a new on-call burden. The team treated observability as a first-class concern.
- Distributed tracing followed a request from the API gateway through the saga coordinator to each store.
- Saga state was stored in a dedicated PostgreSQL table with explicit timeout and retry semantics.
- Each database had its own SLO: p99 write latency for the ledger, p95 read latency for profiles, lag budgets for CDC pipelines.
Cost discipline mattered too. Not every workload needed a multi-region cluster. The team ran PostgreSQL globally for the ledger, but kept regional Cassandra clusters with asynchronous cross-region replication for activity streams. Cheaper, still resilient.
Lessons From Scaling From 10K to 10M
A few hard-won rules emerged from the journey:
- Designate one system of record per fact. The ledger owned balances. Everything else was a projection.
- Push consistency boundaries inward. Keep cross-store transactions small and infrequent.
- Treat CDC as a first-class integration. Streaming changes is more reliable than dual writes.
- Standardize idempotency. One convention, applied uniformly, prevents subtle bugs.
- Invest in platform engineering early. Two engines are a project. Six engines are an internal product.
The result was a hybrid data architecture that grew with the business without sacrificing the correctness regulators expect. Money still moved under serializable isolation. Profiles still evolved without migrations. Activity streams still absorbed billions of events. Nothing broke — because the team chose the right tool for each transaction boundary instead of forcing every workload through a single database.
Polyglot persistence is not a silver bullet, and it is not a trend to chase for its own sake. For a fintech that needed both the rigor of SQL and the flexibility of NoSQL, it was the only path that respected the physics of distributed systems while still moving at startup speed.
