Lesson 3.5 · 11 min read
Everything in this module so far has been about relational databases: tables, rows, columns, ids, relationships. That was deliberate, because relational databases are the correct default for the overwhelming majority of what you'll build, and understanding them deeply is the foundation for understanding everything else.
But they aren't the only kind of storage, and there are real situations where something else is genuinely the right answer. The trouble is that this topic is drowning in fashion. People adopt alternatives because they sound modern, or because a large company uses one, and they end up with a system that's harder to build and easier to break, for no benefit whatsoever. Others avoid alternatives entirely and force their relational database to do things it was never meant to do.
Neither is judgment. So this post is about judgment: what each kind of storage actually is, what problem it genuinely solves, and how to decide. Not a comparison chart to memorize. A way of thinking that holds up when you're the one choosing.
To know when to reach for something else, you have to be honest about how strong the default is. Relational databases have been refined for fifty years, and they offer a set of guarantees that are worth far more than people realize until they lose them.
They enforce structure. Your schema is a promise the database keeps, so bad data cannot get in. They handle relationships natively, which, as the last post argued, is most of what a real product's data actually is. They answer arbitrary questions quickly, including questions you never anticipated when you designed the schema, which is enormously valuable because you will always have questions you didn't anticipate. And they guarantee that operations either fully happen or fully don't, which matters more than any other property when correctness is on the line.
That last one deserves a name, because it's the crown jewel. A transaction is a group of operations treated as one indivisible unit. Move money from one account to another: subtract from the first, add to the second. If the second step fails, the first must be undone, or money simply vanished. A transaction guarantees this. Either both steps happen or neither does. There is no state where the money left one account and never arrived in the other, no matter what crashed or when.
Sit with how strong that guarantee is. In a world of failing networks, crashing servers, and simultaneous users, a relational database will let you say "these three changes are one thing" and hold it. That property is why banks, and everything that resembles a bank, run on relational databases. And it's why "we'll use something else because it's more modern" is usually a bad trade: you're often giving up transactions and getting flexibility you didn't need.
Default to a relational database. Not out of conservatism, but because it's genuinely the most capable general-purpose tool, and because the cost of leaving it should be paid for a real reason.
"NoSQL" is an unhelpful term, because it names a family by what it isn't. It covers several genuinely different kinds of databases that share only the property of not being relational tables. Let's take the ones you'll actually encounter, and what each is truly for.
Document databases store records as flexible documents, essentially JSON, rather than rows in a rigid table. Each document can have a different shape. There's no schema enforced up front, so you can store whatever structure you want, whenever.
The pitch is flexibility, and the pitch is where people get misled. Flexibility sounds free and isn't. What you've done is move the responsibility for structure out of the database and into your code, and into your head. The database will now happily accept a document missing a required field, or with a typo in a field name, or with a number where text should be. It won't stop you, because you told it not to. Every guarantee you gave up is now a rule your code must remember to enforce, forever, in every place, and it will not.
So when does a document database genuinely earn its place? When your data really is document-shaped and doesn't have many relationships. Content that's deeply nested and always retrieved as a whole unit. Records whose shape legitimately varies between instances in ways that aren't just laziness. Event logs, where you're appending varied things and never joining them. Situations where you'd be modeling one document, fetching one document, and never joining across five tables to build a screen.
The honest test: are the relationships in my data central, or incidental? If your product is users and posts and comments and teams, all pointing at each other, all needing to be assembled together to draw a screen, you have a relational problem and a relational database is the right tool. Document databases handle relationships by making you handle them, in application code, without joins, and it's genuinely worse for that shape of problem. If, on the other hand, your data really is a pile of self-contained things with varied structure, the flexibility is real value rather than deferred pain.
Key-value stores are simpler still: a name, and a value, retrieved by name, extraordinarily fast. There's no querying, no relationships, no structure. You ask for a specific key and you get its value, in a fraction of a millisecond, because it's typically held in memory.
This isn't really competing with your main database. It's a different job. Key-value stores are what you use for things that need to be blindingly fast and are fine to lose. Caching, which gets its own treatment in the architecture module, is the biggest use: keep a copy of an expensive result under a key, and serve it instantly instead of recomputing. Session storage, rate limiting counters, temporary state, queues of pending work. All things where speed is everything and permanence isn't the point. Almost every serious product runs a key-value store alongside its relational database, not instead of it. That "alongside" is the important word, and it's the pattern most beginners miss when they think of storage as a single choice.
Other kinds exist and are worth knowing by name so you're not surprised. Graph databases model networks of relationships, where the connections themselves are the primary data and you traverse them (social graphs, recommendation networks, fraud rings). Time-series databases are optimized for enormous streams of timestamped measurements (metrics, sensor readings). Search engines index text for fast, relevance-ranked searching, which is a genuinely different problem from exact-match querying. And vector databases, which we'll cover properly in the AI module, store data in a form that lets you find things by meaning rather than exact match. Each is a specialist. Each is the right answer for the narrow problem it was built for, and the wrong answer for the general case.
There's a category that isn't a database at all, and it's the one builders most often get wrong: where do you put files?
Users upload profile pictures, documents, videos, audio. Where do those go? The tempting answer is "in the database, in a column." Resist this. Databases are built to store and query structured facts, not to hold megabytes of binary data. Stuffing files in makes your database huge, slow to back up, expensive, and bad at the one thing it's good at.
Object storage is the right tool. It's a service built for exactly one job: store a file, give it back when asked, at any scale, cheaply. You upload the file, you get back a URL, and that's it. And then, crucially, you store that URL in your database, in a text column, next to the user it belongs to.
That's the pattern, and it's worth stating clearly because it's so common and so frequently botched: files live in object storage, and the database stores a reference to where the file lives. The database holds facts about the file (who uploaded it, when, what it's called, which post it belongs to) and a pointer to the file itself. Exactly the same instinct as storing a foreign key instead of copying data, applied across systems.
The benefits follow immediately. Your database stays lean and fast. Object storage is cheap and effectively unlimited. And because those files are static, unchanging content, they can be served through a CDN, which, as you learned in the latency post, means they're copied to servers near your users and load almost instantly anywhere in the world. A user's avatar in Jakarta gets served from Jakarta. None of that is possible if the image is a blob sitting inside a database table in one region.
If you take one operational rule from this post: user-uploaded files go in object storage, and their URLs go in the database. Every time.
Here is the decision process, in the order you should run it, for a real product.
Start with a relational database. This is not a compromise or a beginner's choice; it is the strongest general-purpose tool available, and it will be correct for the core of nearly every application you build. Your users, your content, your orders, your teams, all of it belongs in tables with relationships.
Add object storage for files. Not a decision so much as a fact. Files aren't rows.
Add a key-value store when you need speed on things you can afford to lose. Caching expensive queries, sessions, counters, temporary state. Alongside your relational database, never instead of it. Most products reach this point naturally and it's a healthy addition, not a rewrite.
Reach for a specialist when you have a specialist problem, and only then. Real full-text search over lots of content? A search engine, indexing data whose source of truth still lives in your relational database. Genuinely graph-shaped traversal problems, many hops deep? A graph database. Enormous streams of metrics? Time series. Semantic search for AI features? Vectors. In each case, note the pattern: the specialist is usually an addition, holding a derived or specialized copy, while your relational database remains the source of truth.
Consider a document database when your data is genuinely document-shaped and relationship-light. Ask the honest test question. If you find yourself writing code to manually join documents together, you've built a worse relational database inside your application, and you should stop and use a real one.
Notice what this process is. It isn't picking a winner. It's assembling a system where each piece does what it's best at, with a clear source of truth at the center. Real products don't use "a database." They use a relational database for truth, object storage for files, a cache for speed, and perhaps a specialist for one hard problem. That's not complexity for its own sake, it's each tool doing its job, and understanding that shape is what lets you design a system rather than choose a product.
Ask an AI what database to use and it will give you a reasonable answer, often influenced by whatever is currently popular or whatever appeared most in its training data. It won't know your product's shape. It can't know whether your relationships are central or incidental, whether you need transactional guarantees, whether next year's feature will need to join across five tables.
You now have the questions to ask, and they're better than any answer you'd get by default. Are the relationships in my data central? Do I have operations that must fully happen or fully not happen? Am I about to store files in a database column? Do I need this fast, and can I afford to lose it? Am I adding a specialist tool because I have a specialist problem, or because it sounded impressive?
And you have the strongest default in your pocket, which is worth more than knowing every alternative. Start relational. Add object storage for files. Cache when you need speed. Specialize only for a real specialist problem. That sequence, held with confidence, will serve you better across a career than any amount of enthusiasm for whichever database is currently ascendant. The people who get this wrong aren't the ones who didn't know about the alternatives. They're the ones who reached for them without a reason.
Do this before the next post
Take the data model you sketched over the last two posts and run the decision process on it, honestly.
First, look at your relationships. Are they central to what your product does, users owning things, things belonging to projects, comments attached to posts, needing to be assembled together to draw almost any screen? Then you have a relational problem, and now you know that with reasons rather than by default.
Second, find anything in your model that is a file. An avatar, an attachment, an uploaded document, a video. Check where you put it. If it's a column meant to hold the file itself, move it: the file belongs in object storage, and that column should hold a URL. This single correction is one of the most common real fixes in early-stage products.
Third, look for a place where a cache would help. Is there something expensive to compute and frequently requested, a dashboard total, a leaderboard, a homepage feed that's the same for everyone? Mark it. You've just identified where a key-value store earns its place, before you ever need one, and you'll recognize the moment when it arrives.
Finally, ask yourself whether any part of your product is genuinely a specialist problem. Be skeptical. Most aren't. Being able to look at your own system and conclude "a relational database, object storage, and eventually a cache" with real confidence, rather than reaching for something exotic, is a sign that you understand storage rather than just knowing about it.