Every time your API reads or writes data, there is a chain of components working behind the scenes: API → Backend → ORM/Database Driver → Connection Pool → Database → Result. The database driver handles communication, connection pools prevent your application from creating a new connection for every request, and ORMs make database operations easier without eliminating SQL underneath. Understanding this flow is essential once you move beyond building simple APIs.
You write:
const user = await prisma.user.findUnique({
where: { id: 42 }
});
And somehow a database returns the correct user.
It looks like one line of code.
It isn't.
Behind that line, your application has to communicate with another system, obtain a database connection, send a query, wait for the database to execute it, receive the result, and return that connection so another request can use it.
And when your application goes from 10 users to 10,000 users, those details stop being implementation trivia.
They become architecture.
This is the part of backend development that most tutorials hide.
Let's look at what actually happens.
The Request That Started It All
Imagine you're building a blogging platform.
A user opens their profile.
The browser sends:
GET /api/users/42
Your backend receives the request and needs to return:
{
"id": 42,
"name": "Alex",
"email": "alex@example.com"
}
The obvious mental model is:
API → Database → Result
The real system is closer to:
Client
↓
API
↓
Backend
↓
ORM / Database Driver
↓
Connection Pool
↓
Database
↓
Query Result
↓
Backend
↓
API Response
That middle section is where most of the interesting engineering happens.
1. The Database Driver: Your Backend's Translator
Your Node.js, Python, Java, or Go application doesn't automatically know how to communicate with PostgreSQL or MySQL.
It needs a database driver.
For example, a Node.js application can use the PostgreSQL pg driver.
Conceptually:
Application
↓
Database Driver
↓
PostgreSQL
A simplified example:
import pg from "pg";
const { Pool } = pg;
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
The driver handles the database-specific communication.
Your application says:
"Run this query."
The driver takes care of communicating with the database.
This separation is useful because your application doesn't need to understand every low-level detail of the database protocol.
2. Then There Is the Query
Once your application can communicate with the database, it needs to tell the database what it wants.
For example:
SELECT id, name, email
FROM users
WHERE id = 42;
The database receives that query and does the actual database work.
It has to determine:
- Which table is involved?
- Which rows match?
- Should an index be used?
- How should the query be executed?
- What data needs to be returned?
The database then sends the result back to your application.
For example:
[
{
"id": 42,
"name": "Alex",
"email": "alex@example.com"
}
]
Your backend turns that into an API response.
So:
HTTP Request
↓
Backend
↓
SQL Query
↓
Database
↓
Result
↓
Backend
↓
HTTP Response
That's the basic version.
But there's a scalability problem.
3. Why You Can't Just Open a New Connection for Every Request
Suppose your API receives 1,000 requests.
A naive approach would be:
Request 1 → Open Connection → Query → Close
Request 2 → Open Connection → Query → Close
Request 3 → Open Connection → Query → Close
...
It sounds reasonable.
It isn't.
Creating database connections consumes resources. Doing it repeatedly adds unnecessary overhead and can put significant pressure on the database.
Instead, production applications generally reuse connections.
That's what connection pooling is for.
4. Connection Pools: The Unsung Hero of Backend Systems
A connection pool maintains a set of reusable database connections.
Instead of:
Request
↓
Create Connection
↓
Query
↓
Destroy Connection
you get:
Request
↓
Borrow Connection
↓
Run Query
↓
Return Connection
A pool might look like:
Connection Pool
┌─────────────────────┐
│ Connection 1 │
│ Connection 2 │
│ Connection 3 │
│ Connection 4 │
└─────────────────────┘
│
↓
Database
Imagine four requests arrive.
They can use the available connections:
Request A → Connection 1
Request B → Connection 2
Request C → Connection 3
Request D → Connection 4
When Request A finishes:
Connection 1 → Available Again
Another request can reuse it.
The connection doesn't need to be recreated.
5. More Connections Doesn't Mean More Speed
This is one of those backend lessons that becomes important only after you start dealing with production systems.
You might think:
"If 10 connections are good, 100 connections must be better."
Not necessarily.
Your database has finite resources.
Too many connections can create:
- memory pressure
- CPU overhead
- contention
- increased latency
- unnecessary database load
So a connection pool isn't just a performance trick.
It's also a concurrency control mechanism.
For example:
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10
});
The correct pool size depends on your workload, database, server resources, and deployment architecture.
The important lesson is:
Your backend shouldn't be allowed to create an unlimited number of database connections.
6. Where Does an ORM Fit?
Now let's make the architecture more realistic.
Most modern applications don't write SQL for every simple database operation.
You might use an ORM such as Prisma.
Instead of:
SELECT *
FROM users
WHERE id = 42;
you write:
const user = await prisma.user.findUnique({
where: {
id: 42
}
});
Much cleaner.
But don't let the abstraction fool you.
The database hasn't disappeared.
The architecture is now:
Your Application
↓
ORM
↓
Database Driver
↓
Connection Pool
↓
Database
The ORM gives you a more convenient interface for working with the database.
7. ORM Doesn't Mean "No SQL"
This is probably the most important misconception to clear up.
If you use Prisma, Sequelize, TypeORM, or another ORM, you're not avoiding databases.
You're avoiding manually writing every database query.
The underlying system still needs database operations.
So when you write:
prisma.user.findUnique({
where: { id: 42 }
});
the database still has to:
Receive Query
↓
Understand Query
↓
Create Execution Plan
↓
Read Data
↓
Return Result
The ORM simply provides a higher-level way of expressing what you want.
That's why SQL knowledge doesn't become irrelevant when you learn an ORM.
If anything, it becomes more useful when things stop working as expected.
8. Raw SQL vs ORM: The Tradeoff
This isn't really an argument about which technology is "better."
It's about how much control versus abstraction you need.
Raw SQL
SELECT
users.name,
COUNT(posts.id) AS post_count
FROM users
LEFT JOIN posts
ON users.id = posts.author_id
GROUP BY users.id;
You control the query directly.
That's valuable for:
- complex joins
- aggregations
- database-specific features
- performance tuning
- advanced queries
But you're also responsible for writing and maintaining the SQL.
ORM
const users = await prisma.user.findMany({
include: {
posts: true
}
});
This can make everyday development much faster.
You may also get:
- type safety
- migrations
- schema modeling
- autocomplete
- relationship handling
- developer tooling
The tradeoff is abstraction.
The ORM decides how your high-level operation gets translated into database operations.
9. The Approach I'd Actually Recommend
For most application development:
Use the ORM where it makes the code simpler.
But don't treat it as a black box.
If you encounter:
- a slow query
- unexpected database load
- a complicated join
- an inefficient query
- a database-specific requirement
drop down a level.
Think of it like this:
Your Application
│
↓
┌──────────────────┐
│ ORM │
└──────────────────┘
│ │
Simple Queries │
│ ↓
│ Raw SQL
│ │
└────┬───┘
↓
Database Driver
↓
Connection Pool
↓
Database
The goal isn't to eliminate SQL.
The goal is to use the right abstraction for the job.
10. What Happens When the Database Is Slow?
Here's where understanding the architecture starts paying off.
Your endpoint suddenly takes three seconds.
You look at the backend code:
const user = await prisma.user.findUnique({
where: { id: 42 }
});
Nothing obviously looks wrong.
So where is the bottleneck?
It could be:
API
↓
Backend
↓
ORM
↓
Connection Pool
↓
Database
Maybe:
- the connection pool is exhausted
- the query is slow
- an index is missing
- the database is overloaded
- too much data is being returned
- another query is consuming resources
This is why knowing only the ORM isn't enough.
You need to understand the layers underneath it.
11. Indexes: When the Database Has Too Much Data to Search
Imagine your users table contains 10 million rows.
You run:
SELECT *
FROM users
WHERE email = 'alex@example.com';
Without an appropriate index, the database may need to inspect a large amount of data to find the matching record.
With an index, the database can often locate the relevant record much more efficiently.
Conceptually:
Without useful index:
Database
↓
Check many rows
↓
Find matching row
With index:
Database
↓
Index
↓
Matching row
This is why database performance isn't simply about writing faster backend code.
Sometimes the bottleneck is inside the database itself.
12. What About Multiple Operations?
Suppose a user publishes a post.
Your application needs to:
Create Post
↓
Update User Statistics
↓
Create Activity Record
What happens if:
Create Post ✅
Update Statistics ❌
Now your data may be inconsistent.
This is where transactions become important.
Conceptually:
BEGIN
↓
Create Post
↓
Update Statistics
↓
Create Activity
↓
COMMIT
If something fails:
BEGIN
↓
Create Post
↓
Update Statistics
↓
ERROR
↓
ROLLBACK
Instead of treating each operation independently, the database can treat the group as one logical unit.
This is another reason understanding databases matters even if an ORM handles the syntax for you.
13. Things Get More Interesting When You Scale
Imagine your application grows.
You start with:
1 Backend Server
↓
Database
Then traffic increases.
You add more servers:
Load Balancer
/ | \
↓ ↓ ↓
Server Server Server
\ | /
\ | /
Database
Now there's something easy to overlook:
Each application server may have its own connection pool.
Suppose you configure:
20 connections per server
and deploy:
5 backend servers
You could potentially have:
5 × 20 = 100
database connections.
So scaling your application servers can also increase the number of connections reaching your database.
This is why database configuration cannot be considered separately from application architecture.
14. The Production Stack Is More Than "Backend + Database"
A beginner's architecture often looks like:
Frontend
↓
Backend
↓
Database
A production system can look more like:
Client
↓
Load Balancer
↓
Backend Servers
↓
ORM / Driver
↓
Connection Pool
↓
┌───────────┴───────────┐
↓ ↓
Cache Database
↓
Indexes
↓
Storage
And depending on the application, you may also introduce:
- queues
- background workers
- read replicas
- monitoring
- caching layers
- database migrations
- transaction management
The database is not an isolated component.
It's part of a larger system.
15. Common Mistakes Developers Make
Creating a connection for every request
This defeats one of the main benefits of connection pooling.
Setting the pool size arbitrarily high
More connections can eventually make the database less stable, not more performant.
Assuming the ORM automatically creates efficient queries
ORMs are useful abstractions, but developers still need to understand the queries their applications generate.
Ignoring indexes
A perfectly written API can still be slow because the database is doing unnecessary work.
Never learning SQL
This becomes painful when you need to debug a slow query or understand why the database isn't behaving as expected.
Treating the database as a simple storage box
A database isn't just where your JSON happens to live.
It is an engine that manages querying, indexing, concurrency, consistency, transactions, and much more.
The Mental Model That Actually Matters
If you remember only one diagram from this article, make it this one:
API Request
↓
Backend Server
↓
ORM / Driver
↓
Connection Pool
↓
Database
↓
Query Result
↓
Backend Server
↓
API Response
Each layer solves a different problem.
| Layer | Responsibility |
|---|---|
| API | Communicates with clients |
| Backend | Handles application logic |
| ORM | Provides a high-level database interface |
| Driver | Communicates with the database |
| Connection Pool | Reuses and manages connections |
| Database | Stores and processes data |
Once you understand this pipeline, a lot of backend concepts stop feeling disconnected.
Indexes make database lookups more efficient.
Transactions protect consistency.
Connection pools manage database concurrency.
ORMs provide abstraction.
SQL gives you direct control.
Caching reduces unnecessary database work.
They're all pieces of the same system.
Final Take
The interesting thing about database communication is that the code you write is often the smallest part of the story.
You might write:
const user = await prisma.user.findUnique({
where: { id: 42 }
});
But underneath that line is an entire pipeline:
Application
↓
ORM
↓
Database Driver
↓
Connection Pool
↓
Database
↓
Query Execution
↓
Result
When an application is small, you can get away with treating that pipeline as a black box.
When the application grows, you can't.
A slow API might actually be a slow query.
A database outage might actually be an exhausted connection pool.
A scaling problem might actually be too many connections across multiple servers.
And a seemingly harmless ORM operation might generate far more database work than you expected.
That's why learning backend development isn't just about learning frameworks.
It's about understanding what those frameworks are doing underneath the surface.
The best backend developers don't just know how to query a database. They understand what happens between the API request and the row coming back.
And once you understand that journey, the next question naturally follows:
How does the database itself find, organize, and protect all those rows?
That's where tables, relationships, primary keys, foreign keys, indexes, normalization, transactions, and database design begin.
Top comments (0)