Three years ago we chose MongoDB for a fast-growing SaaS product. The reasons were typical: flexible schema for rapidly changing requirements, JSON documents that mapped cleanly to our TypeScript models, and the developer experience of querying JavaScript objects. For the first 18 months, it was a great choice. Then the data model matured, and the cracks appeared.
When Flexibility Becomes a Liability
The pain arrived in stages. First, cross-collection joins required multiple round-trips or the dreaded $lookup aggregation, which became a performance bottleneck as our dataset grew past 50 million documents. Second, the lack of enforced schema meant we had accumulated years of inconsistent data — documents with the same conceptual fields named differently across different time periods of the application. Third, our analytics team couldn't run ad-hoc SQL queries; everything had to be funnelled through application-level aggregations written by engineers.
- Query performance: Complex reports that took 8s in Mongo ran in 0.3s with proper PostgreSQL indexes.
- Data integrity: Foreign key constraints caught bugs in our application code that had silently corrupted data for months.
- Tooling ecosystem: pgAdmin, Metabase, dbt, and every BI tool connect natively to Postgres. MongoDB support is always second-class.
The Migration Strategy
We ran both databases in parallel for 12 weeks. A Kafka-based CDC (Change Data Capture) pipeline replicated every MongoDB write to Postgres in real time. We used this phase to validate data consistency, test the new query layer, and train our team on Prisma ORM. The application was gradually switched over endpoint by endpoint — starting with read-heavy APIs that could safely run against either database.
The hardest part was not the migration itself — it was the data cleansing required to fit years of flexible document data into strict relational schemas. We wrote 40,000 lines of migration scripts, ran them against a production snapshot three times before the live cutover, and kept a rollback playbook ready for 72 hours post-migration. We needed it once, for 11 minutes, before a missed edge case was patched.
What We'd Do Differently
Start relational. The advice to use MongoDB for "flexible early-stage development" has a hidden cost: you'll eventually need to enforce structure, and retrofitting schemas onto existing data is far more expensive than defining them upfront. PostgreSQL's JSONB column type gives you document-store flexibility within a relational system for the parts of your data model that genuinely need it.