Betting Against Evolvability: MongoDB, Joins, and Architectural Commitments
Backend engineer focused on real-world software. I care about clarity, architecture, and decisions grounded in reality. Strong on Java, AWS, automation and AI workflows. I write about engineering without dogma.
Systems don’t stay where they started
In Designing Data-Intensive Applications, Martin Kleppmann uses the term evolvability in a very practical way: real systems tend to not stay where they started.
You design a service with one set of assumptions like “we’ll never need joins”, “this is mostly document-oriented” and a few years later the product, probably driven by the market, has grown in a completely different direction.
New features appear, reporting needs to get more complex, the business wants new combinations of data. The architecture didn’t “fail”; the world simply changed around it. That gap between the original assumptions and the new reality is where evolvability can kill you.
An old problem in a “modern” disguise
Back in the 60s and 70s, we already had hierarchical and network (CODASYL) data models. They were great at following pointers and traversing pre-defined access paths, but they made ad-hoc queries and joins painful.
The relational model flipped that trade-off: normalised tables, SQL, and flexible joins became the default, and for the next decades almost everything became “just use a relational database”.
Decades later, document databases arrived looking new and shiny but in many ways, they simply revived that older set of trade-offs.
MongoDB as a deliberate step back
Document databases like MongoDB are basically a deliberate step back towards those earlier models: they optimise for locality and document-shaped access, not for arbitrary joins. That’s exactly why they can be so fast for the right workload and exactly why they’re such a strong commitment if your data needs change later. It’s a take-it-or-leave-it trade-off.
If your data really behaves like documents, and stays that way over time, MongoDB can be an excellent choice. But that “if” is doing a lot of work.
The bet that didn’t fully price evolvability
In one of the systems I’m working on, that exact bet was made: MongoDB was chosen because, at the time, the product was clearly document-oriented and the access patterns were simple. It was a perfectly rational decision for that moment. Honestly, with the information available back then, it probably felt stupid not to go for it.
One thing wasn’t fully accounted for in that decision: something as inevitable and implacable as tectonic plates: evolvability.
When the product quietly becomes relational
As the product evolved, the data stopped being “just documents”.
Suddenly there were features that needed to correlate entities that had originally lived in separate collections. New reporting requirements appeared, asking questions that sounded suspiciously relational:
“Show me all X that did Y in this time window, grouped by Z.”
“Combine behaviour from service A with attributes stored in service B.”
“Filter by one dimension, aggregate by another, and enrich the result with metadata from somewhere else.”
None of these are exotic queries. They’re just the kind of questions a successful product eventually needs to answer in any professional environment. But in a document-only world, every one of them feels like you’re swimming upstream.
Hand-rolled joins and abusing the tool
At that point, you face a very unglamorous choice. You either accept that your data access patterns have become relational, with all the unpleasant consequences that implies in a document-only database, or you pretend they haven’t and accept even worse consequences, including pushing unnecessary complexity into your code.
Pushing it into the code usually wins in the short term. You start pulling larger datasets than you really should, stitching documents together in memory, writing ad-hoc “joins” across collections in application code, and reinventing half of what a relational database already does for you:
business logic polluted with data-munging glue
services that know far too much about how data is stored
performance that quietly degrades as the product grows
None of this is MongoDB’s fault. It’s just a terrible use of the tool: you picked a database that explicitly avoids joins, and then watched joins slowly turn into an unwanted, unavoidable reality.
The ugly fork: bad joins or painful migration
Once you realise this, you’re already in a bad position. There are only two real options left on the table:
Keep abusing the document database.
Keep writing hand-rolled joins in code, keep over-fetching data, keep pretending the model still fits. You pay in complexity, performance, and team sanity.Migrate to something more relational-friendly.
Move to PostgreSQL or similar, accept the operational and code migration pain, and slowly drag the system towards a model that actually matches how the product behaves today, not how it behaved in V1.
Neither option is fun. That’s the point. When you ignore evolvability, you don’t get to “optimise later”; you just get to choose which kind of pain you’d rather endure.
Evolvability is not an excuse to never take risks
But there’s an important nuance here. The problem is not choosing MongoDB. A specialised tool can be exactly the right choice. The problem is choosing it without being explicit about the bet you’re placing.
It’s one thing to say:
“We know this design trades future relational flexibility for speed and simplicity today. If the product ever needs richer joins, we’re accepting a future migration as a deliberate cost.”
And a very different thing to say, implicitly:
“MongoDB is modern and fits perfectly right now, so let’s just use it,”
and only discover years later that you’ve painted yourself into a corner.
Evolvability shouldn’t paralyse you into always choosing the lowest-common-denominator technology “just in case”. It’s your job as a software professional to keep evolvability in mind and make your commitments consciously: to state, in clear terms, what kinds of future changes you’re optimising for and which ones you’re willing to accept at a high cost if they ever show up.
Architecture as conscious betting
Good architecture is not about avoiding bets. It’s about placing them consciously, with your eyes open, instead of letting the future surprise you.
If you choose a highly specialised tool, do it knowing exactly which futures you’re making cheap and which ones you’re making very, very expensive.