Every 18 months, a new database technology arrives with a compelling pitch: horizontal scale, flexible schemas, developer experience, or some combination of all three. DynamoDB promised internet-scale key-value reads. MongoDB promised schemaless flexibility. Cassandra promised write throughput that relational databases could not touch. Redis promised in-memory speed for everything. Each of these databases is genuinely excellent at solving a specific class of problem. None of them is the right first choice for a South African fintech startup prototype.
PostgreSQL has been in continuous production use since 1996. It is the default database engine for a reason that has nothing to do with nostalgia: it is the most capable general-purpose database that exists, and for the class of problems that startup prototypes actually have, it is almost always sufficient without being limiting.
The NoSQL Hype Cycle and the Problem It Masked
The NoSQL movement emerged from a real problem: relational databases at the scale of Google, Amazon, and Facebook were genuinely constraining. When you are storing billions of user events across thousands of servers, the locking semantics and join overhead of a relational database become genuine bottlenecks.
The mistake was generalising from that problem. A South African fintech startup with 10,000 active users does not have a web-scale problem. It has a business problem — acquiring users, maintaining regulatory compliance, processing payments reliably, and not running out of money. PostgreSQL solves all the database needs of that business. MongoDB does not add value at that scale. DynamoDB adds operational complexity that a small team will spend engineering effort managing rather than building product.
The question to ask when choosing a database is not "what can handle the most data?" It is "what is the simplest technology that will not be the bottleneck between now and 100,000 users?" For 95% of South African startup prototypes, the honest answer is PostgreSQL.
What You Actually Need in a Startup Database
Before picking a database, define the requirements. For a fintech prototype operating in the South African regulated environment, those requirements are:
- ACID compliance — Financial transactions must be atomic. A payment that debits one account must credit another in the same transaction, or neither must happen. PostgreSQL's multi-version concurrency control (MVCC) handles this correctly. Document stores with eventual consistency do not.
- Rich query capability — Regulators, compliance teams, and finance departments ask complex questions. "Show me all transactions above R50,000 by users who registered in the last 90 days and have not completed KYC" is a three-table join. It is a one-line SQL query on PostgreSQL. It is a full data pipeline on DynamoDB.
- JSON support — Semi-structured data is real. Event payloads, third-party API responses, configuration blobs. PostgreSQL's JSONB type handles these natively with GIN indexing for fast lookups. You do not need MongoDB to store JSON.
- Mature tooling — Entity Framework Core migrations, pgAdmin, Flyway, DBeaver, AWS DMS for migrations, AWS RDS for managed hosting, pg_dump for backups. Thirty years of tooling maturity means the answer to almost every operational question is already on Stack Overflow.
- Compliance-friendly audit capability — POPIA requires demonstrable audit trails. PostgreSQL's
pg_auditextension produces statement-level and object-level audit logs that are genuinely useful for regulatory enquiries. This is a native capability, not a third-party add-on.
PostgreSQL Features That Matter for Fintech
Row-Level Security (RLS)
PostgreSQL's Row-Level Security policies allow you to enforce data isolation at the database layer — not just in application code. In a multi-tenant fintech platform (e.g., a white-label lending product with multiple origination partners), RLS ensures that a query from Tenant A's application role physically cannot return Tenant B's records, even if the application has a bug:
-- Enable RLS on the accounts table
ALTER TABLE accounts ENABLE ROW LEVEL SECURITY;
-- Policy: users can only see their own accounts
CREATE POLICY accounts_tenant_isolation
ON accounts
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
-- Application sets the tenant context on each connection
SET LOCAL app.current_tenant_id = '550e8400-e29b-41d4-a716-446655440000';
JSONB for Semi-Structured Data
Credit bureau response payloads, KYC provider webhooks, payment gateway callbacks — these arrive as JSON with evolving schemas. JSONB stores them in a binary format with full GIN index support, meaning you can run WHERE payload @> '{"status": "APPROVED"}' and have it use an index. You do not need a separate document store for this use case.
pgcrypto for Column-Level Encryption
As covered in the POPIA compliance article, pgcrypto enables symmetric encryption of sensitive columns (ID numbers, bank account details) at the database level, with keys managed outside the database via AWS Secrets Manager. The encryption is transparent to the application when the session key is set correctly.
Logical Replication
When you eventually need a read replica for reporting queries (and you will, once your analytics team starts running multi-second aggregate queries that compete with your API), PostgreSQL's logical replication sets it up in under an hour on RDS. You point your reporting service at the replica connection string and your production API is isolated from analytical load.
PostGIS for Location Data
If your product involves any location component — geofencing for merchant presence detection, branch finder, delivery radius — PostGIS turns PostgreSQL into a capable geospatial database. ST_DWithin, ST_Distance, and spatial indexes handle the kind of location queries most fintech products need without adding a separate geospatial service.
Honest Cost Comparison
Technology choices have cost implications. Here is a rough but honest comparison of managed database costs at three scale points, using 2025 AWS pricing in USD (converted to ZAR at approximately R18.50/USD):
At 10,000 active users (~50 GB data):
AWS RDS PostgreSQL db.t4g.medium (Multi-AZ): ~$70/month (~R1,300/mo)
MongoDB Atlas M10: ~$60/month (~R1,110/mo)
DynamoDB (on-demand, 10M reads/writes): ~$15/month but operational complexity cost is real
Verdict: Cost parity at low scale. PostgreSQL wins on operational simplicity.
At 100,000 active users (~500 GB data):
AWS RDS PostgreSQL db.r6g.large (Multi-AZ): ~$280/month (~R5,200/mo)
MongoDB Atlas M30: ~$540/month (~R10,000/mo)
DynamoDB (provisioned, aggressive caching): ~$150/month but requires architectural investment
Verdict: PostgreSQL is materially cheaper. MongoDB Atlas scales cost faster than value for this workload type.
At 1,000,000 active users (~5 TB data, high write throughput):
At this scale, workload characteristics matter more than the database name. PostgreSQL with read replicas and connection pooling (PgBouncer) handles most financial application workloads to this scale. If you are at 1 million active users on a South African fintech product, you have multiple engineering options and the budget to evaluate them properly.
When PostgreSQL Is NOT the Right Choice
Being honest matters more than being a fanboy. There are genuine use cases where PostgreSQL is the wrong tool:
- Real-time gaming leaderboards — If you need sub-millisecond sorted set operations on millions of concurrent writes, Redis is the right answer. PostgreSQL cannot compete on this specific workload.
- Time-series at massive scale — If you are ingesting IoT sensor data at millions of events per second, TimescaleDB (a PostgreSQL extension) helps, but a purpose-built time-series database like InfluxDB or Amazon Timestream may be the right choice above a certain ingestion volume.
- Pure document storage with completely unpredictable schema evolution — If your data has no relational structure whatsoever and the schema changes every week in ways you cannot predict, the schemaless flexibility of MongoDB is genuinely useful. Most financial data does not fall into this category.
- Graph traversal — Modelling complex relationship networks (fraud ring detection, social graphs) is significantly easier in a purpose-built graph database like Amazon Neptune or Neo4j. PostgreSQL can do it with recursive CTEs, but it is not elegant above a certain depth of traversal.
If your product genuinely has one of these requirements, use the right tool. Most South African fintech prototypes in 2025 do not have these requirements in year one.
The Entity Framework Core + Dapper Hybrid Pattern
This is the pattern every Foundation Sprint prototype ships with, and it has proven itself across multiple real engagements. EF Core handles the things it is genuinely good at: migrations, entity mapping, CRUD operations with change tracking. Dapper handles the things EF Core is awkward at: complex multi-table reporting queries, bulk operations, and anything where you want to write the SQL yourself.
// EF Core for standard CRUD — generates safe parameterised SQL
public async Task<Application> CreateApplicationAsync(Application application)
{
_context.Applications.Add(application);
await _context.SaveChangesAsync();
return application;
}
// Dapper for complex reporting — write the SQL you actually need
public async Task<IEnumerable<LoanPortfolioReport>> GetPortfolioSummaryAsync(
Guid tenantId, DateOnly reportDate)
{
const string sql = """
SELECT
p.product_name,
COUNT(a.id) AS total_applications,
SUM(a.loan_amount) AS total_disbursed,
AVG(a.interest_rate) AS avg_rate,
COUNT(a.id) FILTER (WHERE a.status = 'DEFAULT') AS defaults
FROM applications a
JOIN products p ON p.id = a.product_id
WHERE a.tenant_id = @TenantId
AND a.disbursed_at::date <= @ReportDate
GROUP BY p.product_name
ORDER BY total_disbursed DESC;
""";
using var conn = _connectionFactory.CreateConnection();
return await conn.QueryAsync<LoanPortfolioReport>(sql,
new { TenantId = tenantId, ReportDate = reportDate });
}
The rule is simple: if EF Core generates the query correctly and efficiently, use EF Core. If you find yourself fighting with .Include() chains and AsNoTracking() to produce a query that is still not what you want, switch to Dapper and write it. Both live in the same repository, share the same connection factory, and are tested the same way.
The Investor-Ready Database Argument
This is not a soft argument. It is commercial reality in the South African market. When a fintech startup goes through due diligence for a seed round, a banking partnership, or a payment service provider onboarding process, the technical reviewers are financial systems professionals. They understand relational schemas. They can read an entity-relationship diagram. They know what a foreign key constraint is and why it matters.
A PostgreSQL schema with proper normalisation, clearly named tables, foreign key constraints, and documented indexes communicates engineering discipline. A DynamoDB single-table design, while valid and even elegant for the right workload, requires significant explanation and raises questions about whether the team understood the trade-offs they were making. A MongoDB collection with 47 different document shapes because the schema "evolved organically" is a due diligence red flag.
The auditors who review your system before a Deloitte sign-off or an FSP licence application understand SQL. They have been reading it for 20 years. Give them a database they can read.
The Bottom Line
PostgreSQL is not the default choice because of inertia. It is the default choice because it is genuinely excellent at the things South African fintech startups need: ACID transactions, rich queries, POPIA-relevant security controls, mature tooling, and a total cost of ownership that does not scale faster than your revenue. When a specific workload genuinely requires a different tool, use the right tool. Until then, stop chasing novelty and start shipping.
See the full reference stack in action
Every Foundation Sprint delivers a production-grade prototype on the same reference stack: .NET 10, PostgreSQL on AWS RDS, ECS Fargate, and Terraform — all wired together from day one.
See our full reference stack