Why We Chose PostgreSQL Over a NoSQL Store

The meeting that decided this ran about forty minutes. We had a greenfield ledger service to build, a payments platform that would eventually touch a few...

Originally published onanselmfowel.com

The meeting that decided this ran about forty minutes. We had a greenfield ledger service to build, a payments platform that would eventually touch a few hundred thousand merchant accounts, and a room full of engineers with strong opinions. One of the senior backend folks had come in ready to fight for a document store. He had used one at his last shop, liked the developer ergonomics, and made a genuinely good case. We picked PostgreSQL anyway. Two years later I have not once regretted it, and I want to explain why in a way that is more useful than "SQL good, NoSQL bad", because that framing is lazy and mostly wrong.

Why We Chose PostgreSQL Over a NoSQL Store
Why We Chose PostgreSQL Over a NoSQL Store

NoSQL stores are excellent tools. I have run production Cassandra clusters and I would reach for DynamoDB again in a heartbeat for the right workload. This is not a takedown. It is the reasoning of a fintech CTO who has to answer to auditors, a risk committee, and an on-call rotation, and who concluded that for the money-moving core of our business, a relational database was the boring correct choice.

The Shape of Our Data Was Relational From Day One

The first question I ask about any datastore decision is not about scale or performance. It is: what does the data actually look like? Our domain is merchants, accounts, transactions, settlements, disputes, and the relationships between them. A single settlement batch references hundreds of transactions, each of which belongs to a merchant, each of which sits under a legal entity, each of which may have a dispute attached. That is a graph of foreign keys wearing a trench coat. Textbook relational model.

The document-store pitch is that you denormalize this into fat aggregates and read them in one shot. That works beautifully until the day product asks a question that crosses your aggregate boundaries. "Show me every transaction for merchants onboarded in Q2 whose disputes exceeded 1.5% of volume." In Postgres that is a join and a group-by I can write in ten minutes. In a document store where I had optimized the layout for reading a single merchant's dashboard, that same question meant a full scan and a pile of application-side stitching. I have watched teams paint themselves into that corner, and the escape route is always an expensive migration.

In Payments, Transactions Are Not a Nice-to-Have

Here is the part that ends the debate for me in regulated fintech. When I move money between two accounts, I need the debit and the credit to both happen or neither to happen. That is not a preference. If a crash leaves a debit written and a credit lost, I have manufactured money out of nothing, and I will be explaining that to a regulator. Multi-row, multi-table ACID transactions with real isolation guarantees are the thing I need, and Postgres gives them to me with decades of hardening behind them.

Many NoSQL stores have added transaction support over the years, and some of it is genuinely good. But it is often scoped, caveated, and slower on the paths that matter, and the isolation semantics are frequently weaker than they first appear. I did not want my most junior engineer to have to understand the precise consistency footnotes of a distributed document store before they could safely write a balance update. With Postgres, the mental model is one most engineers already carry.

BEGIN;

UPDATE accounts
   SET balance = balance - 5000
 WHERE id = 'acct_source'
   AND balance >= 5000;

-- if the row count is 0, the funds were not there; roll back
UPDATE accounts
   SET balance = balance + 5000
 WHERE id = 'acct_dest';

INSERT INTO ledger_entries (txn_id, source, dest, amount_cents)
VALUES ('txn_9f2', 'acct_source', 'acct_dest', 5000);

COMMIT;

That block either fully commits or leaves the database untouched. The guarantee is not something I bolted on in application code with retries and prayer. It is the database doing its job.

Constraints Are the Cheapest Bug Prevention You Will Ever Buy

I have a strong bias here, and I will state it plainly: I want the database to reject bad data, not trust the application to never send it. Applications have bugs. New services get wired in by engineers who did not read the original spec. A schema with real constraints is a contract that holds regardless of which client is talking.

We lean on this constantly. A few of the guarantees we push down into the database rather than hoping for in code:

  • A check constraint that forbids negative balances on custodial accounts, so a logic error surfaces as a failed write instead of a silent overdraft.
  • A unique index on the idempotency key of incoming payment requests, which is the entire reason a retried webhook does not double-charge anyone.
  • Foreign keys that refuse to let a transaction reference a merchant that does not exist, catching a whole class of bugs at insert time.
  • Not-null and enumerated status columns so a settlement cannot quietly land in a state nobody defined.

In a schema-optional store, every one of those becomes application code, and application code is exactly where the drift creeps in. I once inherited a service backed by a schemaless collection where three different producers wrote the same "status" field with three different capitalizations. Reconciling that mess took a sprint. A single enum column would have made it impossible.

The Scale Story You Are Sold Is Usually Not Your Story

The strongest argument for NoSQL is horizontal scale, and it is a real argument. If you are genuinely operating at a scale where a single primary cannot hold your write volume, that changes the calculus. But be honest about whether that is you. Our peak is a few thousand writes per second with headroom to spare, and a well-provisioned Postgres primary with read replicas eats that without breaking a sweat.

Most teams reaching for a distributed datastore to solve a scale problem are solving a scale problem they will not have for five years, at the cost of complexity they have today. I would rather buy a bigger machine now and shard when the numbers actually force my hand.

Vertical scaling is deeply unfashionable, and I do not care. The hardware you can rent in 2026 is absurd. When our largest table crossed the point where sequential scans hurt, the fix was partitioning by month and adding two indexes, not re-architecting onto a new datastore. That is a Tuesday afternoon, not a quarter-long project. Postgres also gives you an off-ramp: logical replication, partitioning, and mature tooling like Citus exist for the day genuine horizontal scale arrives. You are not trapped.

Postgres Does Documents Too, When You Actually Need Them

The false dichotomy in these debates is that you either get rigid tables or flexible documents. Not true. The jsonb type has been production-grade for years, and we use it deliberately for the parts of our data that really are schemaless: the raw webhook payloads from card networks, provider-specific metadata that varies by integration, feature flags on an account.

So the merchant record has typed, constrained columns for the fields I will join and report on, and a jsonb column for the provider-specific junk drawer that changes shape depending on who we integrated with. I can index into that JSON with a GIN index and query it. I get the flexibility exactly where I want it and the guarantees everywhere else. That hybrid is genuinely hard to beat, and it means the "but our data is unstructured" objection rarely survives contact with the actual requirements.

I Optimize for Operational Boredom

The most underrated property of a datastore is how boring it is to run at 3am. Postgres is boring in the best possible way. The failure modes are well understood, the error messages have been seen by ten thousand engineers before me, and any problem I hit has a Stack Overflow answer from 2014. When something does go wrong, my on-call engineer is not the first human in history to encounter it.

Backups, point-in-time recovery, monitoring, connection pooling with PgBouncer, query analysis with EXPLAIN ANALYZE — all of it is mature and documented to death. Every managed cloud offers a solid Postgres product, so I am not locked to one vendor's proprietary API. Compare that to operating a self-managed distributed store, where a rebalancing event, a hot partition, or a subtle clock-skew bug can turn into a genuinely novel outage that nobody on the team has debugging intuition for. I have lived through one of those: a six-hour incident, three engineers, and a root cause we only half understood at the end. Once was enough.

Every Engineer I Hire Already Knows SQL

This is a soft factor that turns out to be enormous. When I onboard a new backend engineer, they already know how to write a join, reason about an index, and read a query plan. SQL is a skill people bring with them. It has been taught for forty years. I do not have to spend three weeks getting someone fluent in the particular query dialect and consistency quirks of a specific NoSQL product before they can be trusted near the ledger.

That knowledge is also durable. The distributed datastore that was fashionable when I started my career is not the one that is fashionable now, and it will not be the one that is fashionable in ten years. SQL and the relational model have outlived every "SQL is dead" essay ever written. Betting the core of a financial platform on skills my team already has, and will still have in a decade, is just risk management. Boring, defensible risk management.

What Would Actually Change My Mind

I try not to hold beliefs I cannot argue against, so here is when I would pick differently. If I were building a high-volume event or telemetry pipeline where each record is independent, write throughput is enormous, and I never need cross-record transactions, I would reach for something purpose-built and not feel a flicker of doubt. Time-series and analytics workloads are their own world. We run some of those, and they do not live in the primary Postgres.

The point is that the right answer depends on the shape of the workload, not on which technology is trending. Our money-moving core is relational, transactional, and heavily queried across entities, so it belongs in a relational database. Our raw event firehose is append-only and independent, so it lives somewhere else. Using the right tool for each is not a contradiction. It is the whole job. The mistake I see teams make is picking one datastore as an identity and then contorting every workload to fit it.

Anselm Fowel, CTO and fintech architect
Anselm Fowel — CTO & fintech architect

Conclusion

If I could give one piece of advice to an engineer making this call, it would be this: choose the database you can defend to an auditor at 9am and debug alone at 3am, not the one that will impress people at a conference. For a regulated payments platform, that is overwhelmingly Postgres, and the burden of proof should sit on anyone arguing to move the ledger off it. The exciting choice and the correct choice are rarely the same, and in fintech, correct wins. My engineer who wanted the document store, by the way, is now one of Postgres' loudest advocates on the team. Nothing converts a skeptic like a clean incident review where the database did exactly what it promised.

Chat with us