Three completely separate things. Click each to understand it.

Layer 1Driver — speaks the raw protocol
Sits inside your app. Handles TCP connections, auth handshake, serialisation. The lowest level.
psycopg2 · pymongo · mysql-connector · redis-py · boto3 (DynamoDB)
↓ calls into
Layer 2ORM / ODM — maps your objects to queries
Also inside your app. Turns Python classes into SQL or document operations. Uses the driver underneath.
SQLAlchemy · Prisma · Django ORM · Mongoose · Beanie · Tortoise
↓ sends requests to
Layer 3Wire protocol adapter — network-level impersonation
Sits outside your app on the network. Pretends to be the database. Your app and driver don't know it's there.
ExtendDB · FerretDB · Neon · PlanetScale · Dragonfly · CockroachDB
↓ stores in
Actual database
PostgreSQL · MongoDB · MySQL · Redis · SQLite · etc.
Click any layer above to understand exactly what it does and where it lives.

The invisible three

A CockroachDB migration "should work because it's just Postgres" — until it doesn't. A DocumentDB query silently fails because the operator isn't implemented. A Dragonfly deployment mysteriously delivers 25x throughput and nobody on the team can explain why. Three production surprises, one root cause: most developers conflate the three distinct layers that sit between their application code and the database.

Here's a question most senior engineers can't answer cleanly without thinking: what is actually happening between your await session.execute(query) and the bytes hitting the database server? A driver. An object mapper. And — sometimes — a wire protocol adapter. Once you can name each layer, the surprises above become predictable.

The three layers are not complexity for its own sake. They exist because three different problems need solving independently — protocol, productivity, and portability — and bundling them produces the worst of all three. The Principal Engineer move isn't memorizing the layers. It's knowing which one you're operating in at any moment, and knowing when to drop down one.

Your application process
ORM / ODM
SQLAlchemy · Beanie · Mongoose
query string
Driver / SDK
psycopg2 · pymongo · redis-py · boto3
the wire
bytes (wire protocol)
Network / server side
The real database
PostgreSQL · MongoDB · Redis · MySQL…
— or —
Wire protocol adapter
Speaks the same bytes → different storage underneath
Same bytes. Different fate. Your driver cannot tell the difference.

By the end of this post, you should be able to answer, for any database call your application makes: which layer am I in right now, and which layer would solve this problem better?


The driver: the byte mover

The driver is where your application physically touches the database. Everything above it is convenience. Without a driver, your application has no way to reach the database — none. Your ORM doesn't talk to PostgreSQL. Your repository pattern doesn't talk to MongoDB. The driver is the only thing in your stack that physically speaks the database's wire protocol. The driver talks to the database. Everything else talks to the driver.

Mechanically, a driver does exactly four things. It opens a TCP socket to the database server. It performs the authentication handshake. It serialises your queries into whatever binary or text format that specific database expects on the wire. And it deserialises the response bytes back into something your language can use. That's the entire job. It doesn't know about your domain objects. It doesn't know about your schema. It moves bytes in a protocol it was built to speak.

For PostgreSQL in Python, that's psycopg2 (synchronous, battle-tested, libpq under the hood), asyncpg (async-native, written in C and Python from scratch, significantly faster for high-concurrency workloads), or psycopg3 (the official next generation, with async support and binary protocol by default). For MongoDB, it's pymongo synchronously or motor for asyncio. For Redis, redis-py. For DynamoDB, boto3. For SQL Server, pyodbc or pymssql. Different languages, same shape — every database has one canonical wire protocol, and one or more language bindings that speak it.

One naming clarification worth making explicitly, because it confuses people: "driver," "SDK," and "client library" mean the same thing. pymongo and "MongoDB Python client" and "MongoDB SDK for Python" are three labels for one library. The terminology shifts with marketing fashion, not with capability.

The drivers themselves are versioned, occasionally have breaking changes when the database's protocol version changes, and have real performance differences worth knowing. mysqlclient (C extension over libmysqlclient) is meaningfully faster than PyMySQL (pure Python) — if you're benchmarking, this matters. asyncpg outperforms psycopg2 by a comfortable margin under concurrent load, but only if your application is async end-to-end. These are not interchangeable parts. They are the same protocol implemented with different performance characteristics.


The ORM: vocabulary sugar that pretends to be more

This is where the conflation usually lives.

SQLAlchemy is not PostgreSQL. Mongoose is not MongoDB. Django's ORM is not your database. I've watched smart engineers argue about "PostgreSQL behavior" for an hour before realizing they were actually arguing about SQLAlchemy's session-flushing semantics. The ORM is a library inside your application process. It sits on top of the driver. Its entire mechanical reality is this: you write Python (or TypeScript, or Java) in your language's natural idioms, the ORM translates that into the right query language — SQL for relational databases, BSON operations for MongoDB — and then it hands the query to the driver, which sends it over the wire.

What you write
User.query
  .filter_by(
    email=email
  )
  .first()
Python · SQLAlchemy ORM
translates
What the ORM produces
SELECT
  users.id,
  users.email,
  users.created_at
FROM users
WHERE users.email = $1
LIMIT 1
SQL string · dialect-aware
serialises
What the driver sends
[Bind]
  $1 = 'mihir@…'
[Execute]
  rows → objects
[Sync]
PostgreSQL wire protocol · psycopg2

That's worth sitting with. The ORM doesn't talk to the database. The ORM builds a string and gives it to a driver. Swap the driver, point at a different database, and the same ORM code works against MySQL instead of PostgreSQL. That's not magic. It's an indirection layer producing a different SQL dialect for a different driver. SQLAlchemy with postgresql://... and SQLAlchemy with mysql://... are the same ORM, talking to different drivers, producing different SQL.

So what does the ORM actually buy you? Real things. Schema migrations through Alembic or Django's migration framework. Relationship loading — eager, lazy, joined, subquery — that you'd otherwise hand-code. Query construction through method chains that compose. Model validation. Type safety in TypeScript-first ORMs like Prisma. For the 80% of queries that are straightforward CRUD with a couple of joins, the ORM is unambiguously a productivity win.

And it buys you new problems. The N+1 query problem is the canonical example: your ORM loads a list of users, and then issues a separate query for each user's posts, instead of a single JOIN. You don't notice in development with ten rows. You notice in production with ten thousand. The abstraction has leaked. Complex joins — recursive CTEs, window functions, lateral joins — are where ORMs go from helpful to actively obstructive; eventually you find yourself writing text("...") strings and questioning what the abstraction was for. Schema migrations across a team produce conflicts that the ORM can't help you resolve because they're really merge conflicts in generated SQL.

The honest framing: ORMs are productivity tools for application developers. Drivers are precision tools for systems engineers. Knowing when to drop from the ORM down to the driver — when the abstraction is costing you more than it's giving you — is a senior engineering skill. Junior engineers reach for the ORM for everything. Mid-level engineers reach past the ORM when it hurts. Principal engineers design the data layer so the choice is local: easy stuff stays in the ORM, hot paths go raw, and nothing about the architecture forces a choice between them.

One more piece of vocabulary: when the database is a document store rather than a relational store, the same concept is called an ODM — Object Document Mapper. Mongoose, Beanie, MongoEngine. Same idea: vocabulary sugar over a driver, with schemas and validation layered on top. The name changes because there's no "relational" aspect to map. The mechanical reality is identical.

And the ODM has its own particular criticism worth airing: critics argue that MongoDB ODMs paper over MongoDB's schemaless nature and produce the worst of both worlds — schema rigidity without relational power. If you wanted enforced schemas and typed relationships, the argument goes, you should have picked a relational database. The criticism lands. The ODM mental model fits MongoDB worse than the ORM fits PostgreSQL — and if you find yourself needing Beanie or MongoEngine to feel safe writing against MongoDB, it's worth asking whether you picked the right database for the job.

Some databases reject the mapper layer entirely, and they're right to. Redis has no tables, no schema, no rows, no joins — the ORM mental model is irrelevant. The whole point of Redis is direct, predictable command-level access. Redis with an ORM is Redis with the speed bumps reinstalled. DynamoDB is similar: the access pattern constraints (you must specify the partition key) make ORM-style "query any column" abstractions architecturally dangerous, not just slow. Thin mappers exist for both — redis-om, PynamoDB, ElectroDB — but they're acknowledged as thin, and they don't try to be Django.


The wire protocol adapter: the impersonator

Here is the layer most developers have never named, even though many have used it. Once you see it, you can't unsee it.

The wire protocol adapter lives outside your application entirely. It's not a library you import. It's a separate process running somewhere on the network — sometimes on a server you manage, more often on infrastructure a cloud provider runs. Its job is to listen on a port, speak the exact same bytes the real database would speak, accept connections from your driver, and translate the requests internally into operations against a completely different storage backend.

A — what your driver thinks is happening
Your app
FastAPI service
call
Driver
psycopg2
PG wire bytes
PostgreSQL server
the real one, presumably
rows
Storage
disk
B — what's actually happening
Your app identical
FastAPI service
call
Driver identical
psycopg2
PG wire bytes
Speaks PG protocol
CockroachDB / Neon / Aurora
internally
Different storage
distributed / serverless
Your application code is byte-identical in both scenarios.
The connection string is the only line that changed.

Your application doesn't know. Your driver doesn't know. Your ORM doesn't know. None of them can detect the adapter is there, because by the time the bytes get back to your driver, they look exactly like what PostgreSQL or MongoDB or Redis would have sent. The deception happens before your SDK even processes a response.

The distinction from a driver is sharp and worth holding onto: a driver connects to a database. An adapter pretends to be a database. A driver is on your side of the wire. An adapter is on the other side.

And the practical consequence of that distinction is the superpower of this layer: zero application code changes. You don't refactor. You don't swap libraries. You don't rewrite queries. You change a connection string. Your psycopg2 still imports the same. Your SQLAlchemy models are untouched. Your queries execute the same. The only thing that changed is which host and port your driver opened a socket to.

Each adapter implements exactly one protocol and impersonates exactly one database type. An adapter that speaks DynamoDB's HTTP/JSON + SigV4 protocol cannot accept MongoDB client connections — those are different bytes on the wire. There is no universal database adapter, because there is no universal database protocol. Compatibility is per-protocol, not per-concept.

You almost never build these in application code. You encounter them when a cloud provider or platform team deploys one, and your job is to point your existing connection string at the adapter's host. CockroachDB speaks PostgreSQL's wire protocol — your psycopg2 connects to it without knowing the data is distributed across consensus-replicated nodes. Neon speaks PostgreSQL — your asyncpg connects to it without knowing it's a serverless architecture that scales to zero. FerretDB speaks MongoDB — your pymongo connects to it without knowing the storage underneath is PostgreSQL rows and jsonb columns. ScyllaDB speaks CQL — your cassandra-driver connects to it without knowing it's a C++ rewrite using the Seastar framework with 10x the throughput.

The pattern repeats across every major database whose protocol is well-documented enough to be reimplemented. PostgreSQL has the richest adapter ecosystem — Neon, CockroachDB, Aurora PostgreSQL, AlloyDB. MySQL has PlanetScale (built on Vitess, originally from YouTube) and TiDB. MongoDB has FerretDB, Azure Cosmos DB, Amazon DocumentDB. Cassandra has ScyllaDB, Amazon Keyspaces, DataStax Astra.

Redis has the most dramatic adapter story of any protocol. Dragonfly, KeyDB, Upstash, Garnet, and Valkey — five RESP-compatible servers — all emerged or accelerated sharply within roughly twelve months of a single licensing decision. RESP is one of the simplest wire protocols ever designed, and that simplicity meant the entire ecosystem could be reimplemented before Redis Inc.'s commercial moat had time to set. The full story is in the next section, because it deserves one.

The protocols whose adapters are sparsest — SQL Server's TDS, Oracle's — are the ones with the most proprietary legal friction around reimplementation.

That last point isn't incidental. It's the whole story of why this layer looks the way it does.


Four stories the adapter layer tells

The reason wire protocol adapters exist where they exist, and don't exist where they don't, is not technical. It's a story about licensing, competitive dynamics, ecosystem ownership, and what databases were originally designed to do. Four stories worth telling.

Redis and RESP after SSPL

In 2024, Redis Inc. relicensed Redis from BSD to SSPL — the same Server Side Public License that drove MongoDB's own license change years earlier. SSPL is a source-available license that effectively prevents cloud providers from offering managed Redis as a service without releasing their entire service stack under SSPL. Commercially, it gave Redis Inc. a stronger moat against AWS ElastiCache and Google's Memorystore.

The community response was immediate. The Linux Foundation forked Redis as Valkey, with backing from AWS, Google, and Oracle. Dragonfly — a Redis-compatible in-memory database with a shared-nothing per-thread architecture claiming up to 25x Redis's throughput on multi-core hardware — accelerated adoption. KeyDB, already a Redis fork with multi-threading, found new attention. Microsoft Research open-sourced Garnet, a RESP-compatible server written in C#.

All of this happened within months. Why so fast? Because RESP — the Redis Serialization Protocol — is one of the simplest database wire protocols ever shipped. +OK\r\n and $6\r\nfoobar\r\n are not hard to parse. Salvatore Sanfilippo designed RESP to be deliberately easy to reimplement, because he wanted Redis itself to be portable. He got his wish, in a way he probably didn't anticipate: the simplicity of the protocol he designed turned out to be the thing that protected the Redis ecosystem from being captured by Redis Inc.'s licensing decisions.

Protocol simplicity correlates with adapter proliferation. Licensing decisions accelerate it. The Redis ecosystem now outlives Redis Inc.'s commercial moat, because RESP is the standard, and RESP is owned by anyone who can parse \r\n.

Within 12 months of the Redis license change — the RESP cascade
March 2024
Redis → SSPL
License change announced
March 2024
Valkey forked
Linux Foundation · days later
March 2024
Garnet open-sourced
Microsoft Research · C# · RESP
2024
Dragonfly momentum
25× throughput claims · RESP
Apr 2024
Valkey 7.2.5 GA
First stable RESP-compatible release
RESP became the standard the moment Redis Inc. stopped owning it.

MongoDB's wire protocol as competitive signal

When Microsoft built Azure Cosmos DB, they didn't build their own document database protocol. They implemented MongoDB's. When Amazon built DocumentDB, they did the same — DocumentDB implements MongoDB's 3.6/4.0 wire protocol on top of AWS's own distributed storage.

Think about what that means. Microsoft and Amazon are companies with the engineering capacity to design any wire protocol they wanted. They each have multiple proprietary database protocols of their own. They chose to implement MongoDB's, on top of storage engines that have nothing to do with MongoDB internally, because the addressable market of "applications already written against pymongo and Mongoose" was too large to ignore.

That's the competitive signal: two hyperscalers chose compatibility over differentiation. It's an unusual move. Hyperscalers normally prefer to own the protocol because owning the protocol owns the lock-in. Implementing someone else's protocol means accepting that the application developer's existing code is the real customer, not the underlying storage.

The counter-note matters too, and it's instructive. Amazon DocumentDB has well-documented protocol gaps — specific MongoDB operators that aren't implemented, behaviors that diverge from real MongoDB under load, query patterns that work in MongoDB and silently fail in DocumentDB. A wire protocol adapter doesn't guarantee semantic equivalence. It guarantees byte-level compatibility for the operations it implements. Anything the adapter doesn't implement is a sharp edge you find in production. "MongoDB compatible" is not "MongoDB," and the gap is where engineering ownership lives.

SQLite, inverted

Every adapter story so far has the same shape: a database with a network protocol grows alternative implementations of that protocol on different storage backends. The adapter is a compatibility layer.

SQLite inverts this entirely.

SQLite has no network wire protocol. It never has. The "driver" — sqlite3 in Python's standard library, better-sqlite3 for Node.js — reads and writes a .db file directly on disk. There is no server process. There are no sockets. There is nothing to listen on a port. SQLite's portability layer is the file format itself, not a protocol.

So what is Turso? What is Cloudflare D1? What is libSQL?

They are adapters that add a network protocol SQLite never had. Turso wraps SQLite in a server process and exposes it over HTTP and WebSockets, adding replication and multi-tenancy. Cloudflare D1 puts SQLite files in Cloudflare's edge network and exposes them through a REST API and a SQLite-compatible client. LiteFS uses a FUSE filesystem to add replication. The whole adapter ecosystem around SQLite is doing the opposite of every other adapter ecosystem in this post — adding capability the database was deliberately designed without, rather than reimplementing a capability for compatibility.

The interesting question this raises: at what point does Turso stop being SQLite? When the network layer becomes the primary interface, when replication is non-optional, when the architecture is distributed by default — what's left of the original SQLite design philosophy? This is one of the more philosophically interesting movements in databases right now, and it's hiding in a layer most developers don't think about as a layer at all.

DynamoDB and the escape hatch

Every other database's wire protocol adapter ecosystem is about joining a bigger world. Neon makes Postgres serverless. PlanetScale makes MySQL distributed. FerretDB makes MongoDB self-hostable on Postgres. Dragonfly makes Redis faster. The adapters add capability the original couldn't provide.

DynamoDB's adapter ecosystem is the only one that's about leaving.

DynamoDB Local is AWS's own single-process implementation for local testing — useful, but explicitly not production-grade. localstack is a broader project that emulates many AWS services locally, DynamoDB among them. ExtendDB is a more recent experiment that implements DynamoDB's HTTP/JSON + SigV4 protocol on top of PostgreSQL. The motivation behind all of these is the same: applications that were written against boto3 and the DynamoDB API are locked into AWS unless something speaks the DynamoDB protocol outside AWS.

This inverts the usual lock-in story. AWS designed DynamoDB to be inseparable from AWS — the protocol uses AWS's SigV4 signing, the consistency guarantees are AWS-specific, the pricing model is AWS-specific, and the data residency is AWS infrastructure by definition. Every adapter that speaks DynamoDB's protocol outside AWS is, in effect, a piece of escape infrastructure. The ecosystem is small, the implementations are incomplete, and the motivation is consistent: get out, or at least be able to develop locally without paying for the cloud.

The reason this matters as a design lesson: a wire protocol can either be a standard (PostgreSQL, MySQL, MongoDB, Redis — protocols whose adapters expand the ecosystem) or a moat (DynamoDB, Oracle, SQL Server — protocols whose adapters either don't exist or are explicitly framed as escape hatches). The difference is partly technical (openness of the spec, complexity of implementation) and partly strategic (whether the protocol owner wants third-party implementations to exist).


RDS, because we have to talk about it

Amazon RDS is not a database. It is not a driver. It is not a wire protocol adapter. The frequency with which engineers refer to "RDS" as if it were itself a database is a small linguistic tell about how the layers blur in practice.

RDS is a managed hosting service for existing databases. When you provision an RDS instance, you choose one of: PostgreSQL, MySQL, MariaDB, Oracle, or SQL Server. RDS then runs actual instances of those databases for you, handling backups, patching, Multi-AZ failover, and monitoring. When your application connects to an RDS PostgreSQL instance, it connects with psycopg2 or asyncpg — the same driver you'd use for any PostgreSQL server. There is no new protocol. There is no new driver. There is no new ORM. RDS sits at the infrastructure layer, not the protocol layer.

Aurora is where this gets interesting, and where RDS earns its place in a post about wire protocol adapters. Aurora MySQL speaks MySQL's wire protocol but uses a custom distributed storage engine that has nothing to do with MySQL's storage layer. Aurora PostgreSQL does the same for PostgreSQL — protocol-compatible at the wire, custom infrastructure underneath. That makes Aurora a wire protocol adapter at the infrastructure layer, sold as a managed service.

Babelfish goes further. Babelfish for Aurora is a feature that lets a single Aurora PostgreSQL instance accept connections from SQL Server clients speaking TDS — Microsoft's Tabular Data Stream protocol — alongside its native PostgreSQL connections. One instance, two wire protocols, simultaneously. Your existing SQL Server applications connect with their existing TDS-speaking drivers, while your new applications connect with psycopg2. The same data. Different bytes on the wire.

The lesson hiding in this is the broader one worth keeping: infrastructure and protocol are orthogonal. You can change the infrastructure underneath a protocol — from self-hosted to managed to serverless, from single-node to distributed, from MySQL storage to a custom storage engine — without changing the protocol your application speaks. And therefore without changing a single line of application code. This is the entire reason the wire protocol adapter exists as a category.

SQL Server clients
pyodbc · Entity Framework
SQL Server Management Studio
TDS protocol
PostgreSQL clients
psycopg2 · asyncpg
SQLAlchemy · Prisma
PostgreSQL wire protocol
Aurora PostgreSQL
+ Babelfish
one instance
custom storage engine
Aurora storage
distributed · replicated
One instance. Two wire protocols. Same data. Different bytes.
Infrastructure and protocol are orthogonal concerns.

The Principal Engineer move

Here's the part worth putting on a sticky note: the same three-layer separation exists inside your application code, at smaller scale. The driver-level call is the raw query in a repository method. The ORM is the model layer with typed queries on top. The wire protocol adapter is the repository interface that your service layer depends on — the service doesn't know whether the repository is backed by PostgreSQL, Redis, an in-memory mock for tests, or a remote API. You designed it that way. The storage backend is an implementation detail, not a dependency.

Every time you add a layer of abstraction, you gain something and trade something. Gain portability, trade performance. Gain productivity, trade transparency. Gain infrastructure flexibility, trade visibility into what's actually happening on the wire. None of these trades are wrong. They're just trades, and naming them clearly is half the work of making them deliberately.

That mirror is what makes the three-layer model worth internalizing. It's not just a vocabulary for talking about databases. It's a design discipline you can apply to any system where one component talks to another over a protocol. Protocol, productivity, portability. Three problems, three layers, three different abstractions to know the cost of.

Next time you're debugging a database problem, ask yourself which of the three layers it actually lives in. Half the time it's not the one you assumed. And when you find yourself reaching for a more powerful abstraction to solve a problem one layer below, ask whether the right move is to go down a layer instead. The senior engineering work is usually closer to the wire than the framework wants you to think.


Appendix: the three layers, by database

Database Driver (Python) ORM / ODM Wire Protocol Adapter
PostgreSQL psycopg2, asyncpg, psycopg3 SQLAlchemy, Django ORM, Tortoise Neon, CockroachDB, Aurora PostgreSQL, AlloyDB
MySQL mysql-connector, PyMySQL, mysqlclient SQLAlchemy, Django ORM PlanetScale, Vitess, TiDB
SQL Server pyodbc, pymssql SQLAlchemy, Entity Framework (.NET) Babelfish for Aurora, Azure SQL
SQLite sqlite3 (stdlib), aiosqlite SQLAlchemy, Django ORM, Tortoise Turso / libSQL, Cloudflare D1, LiteFS
MongoDB pymongo, motor Beanie, MongoEngine, Mongoose (Node) FerretDB, Azure Cosmos DB, Amazon DocumentDB
Redis redis-py, aioredis None (redis-om as thin mapper) Dragonfly, KeyDB, Upstash, Garnet, Valkey
DynamoDB boto3, aioboto3 PynamoDB, ElectroDB, dynamodb-onetable ExtendDB, DynamoDB Local, localstack
Cassandra cassandra-driver (DataStax) cqlengine (bundled) ScyllaDB, Amazon Keyspaces, DataStax Astra
Amazon RDS Same as underlying DB Same as underlying DB N/A — RDS is infrastructure, not protocol

Note on MariaDB: MariaDB is not a wire protocol adapter — it's a fork with its own storage engine that happens to be MySQL-protocol-compatible by origin. PlanetScale and TiDB implement MySQL's wire protocol on top of fundamentally different backends (Vitess and a distributed SQL engine). MariaDB is protocol-compatible because it diverged from the same codebase, not because it adapts a different storage layer. The distinction matters when reasoning about what compatibility guarantees you're actually getting.


Key takeaways:

  • Drivers, ORMs, and wire protocol adapters are three different abstractions solving three different problems — protocol, productivity, and portability.
  • The ORM is not the database. SQLAlchemy is not PostgreSQL. The mechanical reality is that the ORM produces a query string and hands it to the driver.
  • Wire protocol adapters live outside your application and impersonate a database at the byte level — psycopg2 cannot tell the difference between PostgreSQL and CockroachDB.
  • The protocols with rich adapter ecosystems (PostgreSQL, MySQL, MongoDB, Redis) are open and well-documented. The protocols with sparse or escape-only adapter ecosystems (DynamoDB, Oracle, SQL Server) are proprietary or strategically owned.
  • The same three-layer separation belongs in your application code: raw driver call at the bottom, model layer in the middle, swappable repository interface at the top.