When developers start a new project, the database is often treated as an implementation detail.
The conversation sounds simple:
- Should we use PostgreSQL or MongoDB?
- Should we add Redis?
- Which database scales better?
Six months later.
The product gains traction. Traffic hits 2,000 requests per minute. Suddenly, P99 latency spikes to 4 seconds. Your connection pool exhausts every time a scheduled reporting job runs. A single JOIN on a multi-million-row table locks up CPU cores, and your database memory charts skyrocket toward a production crash.
In a panic, the team throws a Redis cluster in front of the database to “cache the slow queries.” When that doesn’t fix the underlying write locks, someone suggests migrating to MongoDB because “Document stores scale better.”
This is the exact moment you realize a painful truth: You didn’t just pick a place to store data. You made an architectural decision that defined your system’s throughput limits, concurrency mechanics, scaling strategy and failure modes—and you did it without thinking.
A Relatable example
Consider a simple e-commerce application.
A customer places an order. The application needs to create the order, record its items, reserve inventory, and initiate payment.
Now ask a few questions:
- What happens if the order is created but inventory reservation fails?
- How do you prevent two customers from purchasing the last available item?
- What happens when the product catalogue grows from 1,000 products to 10 million?
- How efficiently can you retrieve an order with its items, payment status, and delivery information?
- What happens when a query that worked perfectly in development becomes one of your most expensive production operations?
These are not just database questions. They are architectural questions.
And the database you choose determines which of them are straightforward, which require additional engineering, and which become significantly harder to get right.
Let’s look at why.
1. Your Data Model Is Also a Design Decision
One of the first mistakes developers make is choosing a database based on how the data looks rather than how the application needs to use it.
Imagine a product in an e-commerce application:
At first glance, this looks like a simple document. You could store it in a relational database or a document database.
Both can represent it.
But the important question isn’t whether a database can store your data.
It’s how the data relates to everything else in your system.
A product belongs to a category. An order contains products. A customer places orders. A payment belongs to an order. Inventory changes as orders are placed and cancelled.
The data is connected, and those connections carry business meaning.
In a relational database, you might model the relationships explicitly:
This structure makes several decisions explicit.
Products reference categories. Order items reference products. Prices and quantities have defined types. Foreign keys can prevent references to non-existent records.
The schema becomes part of the system’s correctness model.
A document database can also represent these relationships, but the design might look different. You may embed data that is usually retrieved together, reference data that changes independently, or maintain denormalized copies for specific queries.
Neither approach is automatically superior.
The difference is where you place the complexity.
With relational modeling, you often manage relationships through joins, constraints, and transactions. With document-oriented modeling, you may simplify particular reads but take on duplication, update coordination, or additional application logic.
The architectural question here is not which database can represent your data. It’s which data model best supports the relationships and operations your application must guarantee.
2. Start With Access Patterns, Not Database Popularity
Suppose you’re building a SaaS application.
You know you need to store users, projects, tasks, and activity logs.
That tells you what data exists. It doesn’t tell you how the system will access it.
Consider these operations:
- Retrieve a project and its most recent tasks.
- Find all tasks assigned to a user across multiple projects.
- Display the latest 50 activity events for a workspace.
- Calculate monthly usage for billing.
- Search projects by name within a particular organization.
These queries have different requirements.
Some filter by relationships. Others sort by time. Some need aggregations. Others require low-latency lookups.
Your database schema, indexes, and even your choice of storage model should reflect these access patterns.
For example, if your application frequently retrieves recent activity for a workspace, you might query PostgreSQL like this:
A suitable index could be:
Why this index?
Because the query filters by workspace_id, sorts by created_at, and returns only a limited number of rows.
The index can help PostgreSQL locate the relevant workspace records and retrieve recent entries without sorting the entire matching dataset.
The exact benefit depends on the table size, data distribution, query plan, and workload. An index is not a guarantee of a faster query in every situation.
It also introduces costs: additional storage and work during inserts, updates, and deletes.
That is the trade-off.
Now imagine discovering this access pattern after the application is already in production. You may need to change indexes, restructure queries, or redesign the way activity data is stored.
None of that is impossible. But understanding the workload earlier can prevent avoidable complexity.
This is why database design should begin with questions like:
- What are the most frequent reads and writes?
- Which fields are used for filtering and sorting?
- Which relationships must be traversed?
- Which queries must remain fast as the dataset grows?
- Which operations require aggregation or reporting?
- What latency can the application tolerate?
Don’t choose a database because it fits your data. Choose a database architecture that fits the way your system uses that data.
3. Transactions Are Where Database Choices Become Business Decisions
Some operations can tolerate partial completion.
Others cannot.
If a user updates their profile picture and the notification email fails, the system can usually retry the email independently.
But if an order is recorded while its inventory reservation fails, the application may accept a purchase it cannot fulfil.
Consider this simplified NestJS service using Prisma:
The intention is sound: group the inventory update and order creation into one transaction so that a failure rolls back the database changes.
But there is a subtle concurrency problem.
Imagine the product has one item remaining.
Two requests arrive at almost the same time. Both read a stock value of one. Both pass the check. Both decrement the stock.
Depending on the database’s isolation behaviour and the operations performed, a naive read-check-update sequence can allow overselling or produce an invalid stock value.
Wrapping code in a transaction does not automatically make every business invariant safe.
One way to improve this design in PostgreSQL is to perform a conditional update:
The update only succeeds if sufficient stock remains when PostgreSQL evaluates the condition. Concurrent updates to the same row are coordinated by the database, and the predicate is checked against the row being updated.
If the update count is zero, the reservation failed. Throwing an error rolls back the transaction, including any order creation performed within it.
In a real system, you’d also validate that quantity is positive, enforce appropriate database constraints, and handle errors deliberately.
The important distinction is that the database is doing more than persisting information. It is helping enforce a business invariant under concurrency.
This is where database capabilities matter.
You need to understand:
- Atomicity: do related database changes succeed or fail together?
- Isolation: what can concurrent transactions observe?
- Constraints: which invalid states can the database reject?
- Locking: how are conflicting writes coordinated?
- Durability: what guarantees exist after a successful commit?
A database that supports transactions, constraints, and suitable concurrency controls can make these guarantees easier to implement.
Other storage systems may offer different transaction scopes or consistency guarantees. Some can support the same business requirements, but the design and operational trade-offs may differ.
If your application depends on financial correctness, inventory accuracy, or reliable state transitions, transaction semantics are not a minor implementation detail. They’re part of your architecture.
4. Your Database Choice Changes How You Scale
“Will it scale?” is one of the most common questions developers ask about a database.
But scale is not a single problem.
A system may struggle because of:
- Too many concurrent connections.
- Expensive joins or poorly designed queries.
- Missing or ineffective indexes.
- Large write volumes.
- Contention on frequently updated rows.
- Growing storage requirements.
- Read traffic that exceeds the primary database’s capacity.
- Reporting workloads competing with transactional operations.
Different problems require different solutions.
Let’s say your SaaS application starts with a few hundred users and eventually serves tens of thousands.
Your first instinct might be to migrate to a distributed database.
But what if the actual problem is an unindexed query that scans millions of activity records?
What if your ORM is generating dozens of queries per request?
What if your application opens too many database connections?
Changing database engines may introduce more complexity without addressing the bottleneck.
A more sensible progression is to measure the workload, inspect query plans, improve access patterns, and identify the actual constraint.
Depending on the evidence, you might introduce:
- Better indexes and query optimization.
- Connection pooling.
- Caching for frequently accessed data.
- Read replicas for appropriate read-heavy workloads.
- Partitioning for large tables with suitable access patterns.
- Background processing for expensive non-interactive work.
- Sharding or a different storage architecture when simpler options are insufficient.
Each option introduces trade-offs.
A read replica can distribute reads, but replication lag may mean a user doesn’t immediately see a recent write when reading from the replica.
Caching can reduce database load, but it introduces invalidation and freshness problems.
Partitioning can improve certain large-table workloads, but it requires careful key selection and query design.
Sharding can distribute data and load, but cross-shard queries, transactions, rebalancing, and operational complexity become harder.
The right decision depends on the bottleneck you’re solving.
This is also why the database choice matters early: it shapes the available optimization paths, the complexity of future changes, and the skills your team needs to operate the system.
However, your first database decision does not need to predict every future scaling problem.
It needs to support the current workload well without making reasonable future evolution unnecessarily difficult.
5. SQL vs. NoSQL Is the Wrong First Question
The SQL-versus-NoSQL debate often starts with the wrong assumption: that one category is inherently better for modern applications.
Relational databases are not outdated because document databases exist. Document databases are not automatically more scalable because their schemas are flexible.
These systems optimize for different requirements, and their capabilities overlap more than simplistic comparisons suggest.
A relational database such as PostgreSQL is often a strong starting point when your application has interconnected entities, important integrity constraints, complex queries, and transactional workflows.
A document database can be a good fit when your data is naturally document-oriented, access patterns align with document boundaries, and flexible document structures provide a practical advantage.
Key-value stores are useful when the workload primarily involves simple lookups by key and the chosen system provides the required durability and consistency guarantees.
Search engines are designed for sophisticated text retrieval, relevance scoring, and search-oriented indexing.
Time-series systems may be appropriate for workloads involving large volumes of timestamped measurements and time-based analysis.
Notice the pattern: each choice follows the workload and its requirements.
Even these categories are not mutually exclusive. PostgreSQL supports JSONB for semi-structured data, for example, and Redis supports more than basic key-value operations.
You don’t have to adopt a separate database for every data type or feature.
In fact, introducing several databases too early can create more problems than it solves.
Now your team has multiple systems to deploy, monitor, secure, back up, restore, and understand. Data may need to move between them, and cross-system consistency can become an application-level responsibility.
Polyglot persistence — using different storage technologies for different workloads — is a legitimate architectural strategy. But it should solve a concrete problem, not serve as a demonstration of technical sophistication.
For many applications, a well-designed PostgreSQL database is sufficient for transactional data, JSON-based fields, and a substantial range of query requirements.
Start with the simplest architecture that satisfies the requirements. Add another database when you can explain exactly what it provides that your existing system cannot provide efficiently or safely.
6. Operational Complexity Is Part of the Decision
A database is not just an engine you connect to using a connection string.
It is a system you must operate.
Before choosing one, consider what happens when things go wrong.
- How are backups taken?
- Have restores been tested?
- How will schema migrations be deployed?
- How will connection limits be managed?
- How will slow queries be identified?
- What happens during failover?
- Can the team diagnose replication lag or storage pressure?
These questions are easy to ignore when an application is small.
They become important when the application holds customer data, processes payments, or supports a business that cannot afford extended downtime.
Consider connection pooling.
If your application runs multiple NestJS instances, each creating many database connections, the combined number of connections can overwhelm the database even when query volume is reasonable.
Adding more application instances may actually make the situation worse.
Similarly, a migration that works on a small development database may lock a large production table or take longer than the deployment window allows.
Operational requirements should therefore influence your choice.
A managed PostgreSQL service might be preferable to operating a more exotic database yourself. A specialized system may offer worthwhile benefits, but those benefits must justify the extra operational burden.
Evaluate the whole cost:
- Infrastructure and storage.
- Query performance under realistic workloads.
- Backups and recovery.
- Monitoring and debugging.
- Deployment and migration safety.
- Team familiarity and hiring.
- Failure recovery and availability requirements.
The cheapest database to start with is not always the cheapest system to maintain.
And the most powerful database is not necessarily the best one for a team that lacks the experience to operate it reliably.
7. How to Choose a Database Without Guessing
You don’t need to predict every requirement your application will ever have.
You need a repeatable way to make the decision.
Here’s a practical framework.
Step 1: Identify the data and its relationships.
List the main entities, how they relate, and which invariants must always hold. Do customers own orders? Can an order exist without its items? Must every payment reference a valid order?
Step 2: List the important access patterns.
Write down the queries the application will execute most frequently. Include filtering, sorting, joins, aggregation, search, and write operations.
Step 3: Define consistency requirements.
Determine which operations must be atomic, which data can be eventually consistent, and what happens if a read returns a slightly stale result.
Step 4: Estimate the workload.
Think about expected read/write volume, concurrency, data growth, latency targets, and traffic patterns. Estimates do not need to be perfect, but assumptions should be explicit.
Step 5: Evaluate operational constraints.
Consider hosting, backups, recovery objectives, monitoring, migrations, connection management, budget, and your team’s experience.
Step 6: Test the risky parts.
Build a small proof of concept around the most demanding queries and transactions. Use realistic data volumes, inspect execution plans, and test concurrent operations.
Step 7: Record the trade-off.
Document why you chose the database, what assumptions the choice depends on, and what evidence would justify revisiting it.
A decision is much easier to evaluate when the reasoning behind it is visible.
A practical starting point
These are starting points, not universal prescriptions. Workload, consistency requirements, managed-service capabilities, and operational constraints can change the answer.
8. The Best Database Decision Is the One You Can Explain
A good engineer does not choose PostgreSQL because PostgreSQL is popular.
They choose it because its relational model, transactional capabilities, indexing, query flexibility, ecosystem, and operational characteristics fit the system’s requirements.
They don’t choose MongoDB merely because the schema changes frequently. They ask whether the data is naturally document-oriented, whether the access patterns fit the model, and how relationships and consistency should work.
They don’t introduce Redis simply because a request is slow. They first determine whether the database is actually the bottleneck and whether caching is appropriate for the freshness requirements.
The difference is not the technology.
It’s the reasoning behind the choice.
A database decision should be based on what the system needs to guarantee, how it needs to access its data, what workload it must support, and what complexity the team is prepared to own.
You may eventually replace the database. Requirements change, workloads grow, and new constraints emerge.
But you should not have to replace it because the original decision was based entirely on convenience or hype.
9. When to Revisit Your Database Decision
You don’t need to pick the “perfect” database on day one. But you do need to know when your current choice is becoming an architectural bottleneck.
Signals it’s time to rethink:
- You’re adding more caches than features.
- Your migrations regularly take hours or require maintenance windows.
- You can’t explain your consistency model in one sentence.
- Your team is afraid to touch certain tables.
- You’re building custom sharding logic in your application.
At that point, you’re not “tweaking” your database. You’re redesigning your architecture.
Final Thoughts
It’s easy to choose a database when all you’re thinking about is getting your application to work.
The harder question is whether that choice will still make sense when your application has more data, more users, more complex business rules, and less room for failure.
That’s when the real consequences begin to surface.
The schema that once felt simple starts resisting new requirements. Queries that worked well at first become expensive. Concurrent requests expose inconsistencies you never anticipated. And the database you chose in a few minutes starts influencing decisions that take weeks to change.
None of this means you need to predict every future problem or choose the most sophisticated database available. You don’t.
What you need is a clear understanding of your application’s data, access patterns, consistency requirements, and operational constraints.
Sometimes, that understanding will lead you to PostgreSQL. Sometimes, a document database or a specialized storage system will make more sense. And sometimes, the best decision is to stick with what you already have and fix the actual problem.
The difference is that you’re no longer choosing based on hype, familiarity, or what everyone else seems to be using. You’re choosing based on what your system needs.
Good backend engineering isn’t about choosing the most powerful technology. It’s about making decisions whose trade-offs you understand and can justify.
Your database is one of those decisions.
Choose it deliberately.








Top comments (0)