I used to treat databases like magic.
The tutorials always showed the same pattern: create an engine, make a session factory, write a dependency that yields sessions. It worked. I copied it everywhere. But I never really understood why.
Then I started building background workers and suddenly those copy-paste patterns stopped working. I’d stare at my code, confused:
- Why do we keep the database engine around forever?
- Why do we recreate sessions for every request?
- If APIs should be “stateless,” why are we persisting anything?
- Is S3 stateless or stateful? What about Redis?
Something wasn’t adding up.
The Tension That Wouldn’t Go Away
Everyone says: “Build stateless APIs.”
But in practice, every backend I’ve built keeps things around:
- Database engines that live for the entire process
- Session factories stored in app state
- S3 clients tucked into context objects
- Redis connections shared across requests
So which is it? Are we stateless or not?
For a long time, my brain filed this under: “This is just what you do.” Which was really code for: “I don’t understand this, but it seems to work, so let’s move on.”
The Mental Model That Changed Everything
Here’s the picture that finally made sense.
Imagine a horizontal line cutting through your entire system:
Above the line: Your request handlers and background tasks.
- They behave like they’re stateless
- Each request is its own clean unit of work
- No hidden state carried between requests
Below the line: The persistence world.
- Postgres with its tables and rows
- S3 with its buckets and objects
- Redis with its keys and values
- This is where real state lives across everything
On the line: Client objects that connect the two worlds.
- Database engines with their connection pools
- S3 clients with their configuration
- Redis clients with their connection management
- These are the bridges between your stateless code and the stateful world
Once I saw it this way, everything started making sense.
Your code isn’t cheating when it keeps an engine around. You’re not breaking the “stateless” rule. You’re just maintaining the infrastructure to access the stateful world cleanly.
Why Databases Feel So Special
Let’s be honest: databases get way more ceremony than S3 or most other services. There’s a reason for that.
Connection Pools Are Serious Business
Imagine your database is a restaurant with exactly 100 tables.
If every customer (request) brought their own table and left it there, the restaurant would fill up instantly. Even worse, if you have 20 servers (workers) and each brings 10 tables per second… you see the problem.
So instead:
- The restaurant has a pool of tables it manages (the engine)
- Customers sit down, eat (do their work), and leave
- The same table gets reused for the next customer
In code terms:
- One engine per process manages a small pool of connections
- Each request grabs a connection from the pool
- When the request finishes, the connection goes back
This isn’t style. This is survival.
Transactions Need Their Own Space
A database session isn’t just a connection—it’s a transaction boundary.
Think of it like this: a session is a private notepad where you write down everything you want to change. At the end, you either tear out the page and hand it in (commit), or crumple it up (rollback).
If you share that notepad across multiple requests, suddenly you’re mixing everyone’s changes together. One request thinks it saved an order, another thinks it cancelled it, and the database has no idea what you actually want.
That’s why sessions are strictly per-request. One request, one notepad, clean start to finish.
What About S3?
S3 is interesting because it sits on the boundary differently.
Yes, S3 stores files that persist. Upload something today, download it tomorrow. It’s absolutely stateful.
But—and this is the key difference—you talk to S3 over HTTP. Every upload, every download is its own independent request. There’s no “transaction” that spans multiple operations. No notepad that tracks changes before you commit them.
Think of it like mail:
- Database: You’re in a room working on documents. You can see everything you’ve written, make changes, and decide later whether to file them or throw them out.
- S3: You’re sending letters. Each letter is complete and independent. The post office doesn’t remember your previous letters or hold them in some draft state.
Both are stateful systems below the line. But databases need more careful handling because of connections and transactions.
The Real Meaning of “Stateless”
Here’s where my confusion cleared up.
When people say “build stateless APIs,” they don’t mean “never keep anything in memory.”
They mean: each request should not depend on hidden state from previous requests.
In practice:
- ❌ Don’t keep a database session alive between requests
- ❌ Don’t store “current user” in a global variable
- ❌ Don’t rely on some mutable object that changes across requests
- ✅ Do keep shared infrastructure (engines, clients, config)
- ✅ Do create fresh sessions/contexts for each request
- ✅ Do read and write actual state from databases, caches, and storage
The shared objects are tools, not business state. Your real state lives below the boundary, where it belongs.
When This Really Mattered: Background Workers
All of this became concrete when I started building background workers.
In a web framework, you get this for free. The framework handles creating sessions per request and cleaning them up. You mostly just write your business logic.
But in a background worker? Suddenly you have to decide:
- What goes in the worker’s context (shared across all tasks)?
- What gets created fresh for each task?
It’s the boundary decision, made explicit.
A clean pattern:
# In worker setup (long-lived)
context["db_session_factory"] = SessionFactory(engine)
context["s3_client"] = S3Client(config)
# In each task (short-lived)
async def process_document(context, document_id):
session = context["db_session_factory"].create()
s3 = context["s3_client"]
# Do work with fresh session
# Session tracks its own transaction
# Then it's done and cleaned up
The factories and clients live on the boundary. The session and your business logic live above it, clean and independent.
Why This Mental Model Is Worth Having
I didn’t realize I was on autopilot until I had to wire things myself.
The FastAPI tutorial says: “Do this.” So I did it. The Litestar docs say: “Put this here.” So I put it there.
And that works… until you step off the happy path:
- Multiple workers
- Background tasks
- Mixing databases, S3, Redis, message queues
- Debugging weird connection issues
At that point, copy-paste stops being enough. You need to understand why things are shaped the way they are.
For me, the breakthrough was seeing the boundary:
- Below: Persistent systems (Postgres, S3, Redis) where real state lives
- On: Client objects (engines, session factories, S3 clients) that bridge the gap
- Above: Stateless-ish request handlers that do work and move on
Once you see it, a lot of “magic patterns” become obvious design decisions.
You’re not keeping a database engine around because of some weird Python quirk. You’re keeping it around because creating connection pools is expensive and they’re meant to be shared.
You’re not creating sessions per request because the tutorial said so. You’re doing it because transactions need clean boundaries and connection pools need to reuse connections.
You’re not being inconsistent when you share an S3 client but create fresh database sessions. You’re recognizing that different systems have different interaction models.
TL;DR
If you’re coming back to this later and just need the core ideas:
Stateless doesn’t mean no shared objects. It means each request doesn’t rely on hidden mutable state from previous requests.
The boundary model:
- Below: Persistent systems (databases, S3, Redis)
- On: Client infrastructure (engines, session factories, clients)
- Above: Per-request logic (sessions, services, handlers)
Databases feel special because:
- Connection pools have hard limits
- Transactions need clean boundaries
- Sessions track changes before commit/rollback
In practice:
- Share: engines, clients, configuration, factories
- Create fresh: sessions, request contexts, transaction boundaries
The next time you see engine, sessionmaker, Session, or s3_client, you’ll know where they live in your architecture and why they’re shaped that way.
Not just magic patterns. Conscious design decisions about where the line is between your code and the stateful world below it.
(Written by Human, improved using AI where applicable.)
