AlgoMaster Logo

JDBC Drivers

Low Priority22 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

A JDBC driver is the piece of code that turns Java method calls like connection.prepareStatement(...) into the wire bytes a specific database understands. The previous lesson covered the JDBC architecture at the level of "your code calls an API and a driver talks to the database." This lesson goes one layer down: what kinds of drivers exist, how the JVM finds them, what a JDBC URL actually says, and how DriverManager picks the right driver for a given URL.

What a Driver Actually Does

When your code calls DriverManager.getConnection("jdbc:h2:mem:shop", ...), the java.sql.Driver implementation on the classpath is the piece that knows how to open a TCP connection (or an in-process memory store) to that specific database, perform the database's handshake, authenticate, and return a Connection object backed by code that speaks the database's protocol. JDBC itself is a set of interfaces. The driver is the implementation.

Every database has its own driver: H2 ships org.h2.Driver, PostgreSQL ships org.postgresql.Driver, MySQL ships com.mysql.cj.jdbc.Driver, Oracle ships oracle.jdbc.OracleDriver. The driver jar is just an ordinary Java library you put on the classpath. The same JDBC code you write against H2 can talk to PostgreSQL by swapping the jar and the URL.

The diagram shows where the driver sits. Application code never calls driver-specific classes directly; it only touches the java.sql interfaces. The driver is plumbing the JVM discovers and wires up. With a standard API, switching databases means swapping a jar and changing a URL, not rewriting application code.

Not all drivers are built the same way. Over the years the JDBC spec has recognized four different driver "types," each describing a different strategy for getting from Java code to database wire bytes. Three of those types are mostly historical at this point, but they still show up in interview questions and in legacy systems.

The Four JDBC Driver Types

The JDBC specification categorizes drivers by how they reach the database. The types are numbered 1 through 4, and they line up roughly with the history of how the ecosystem evolved. Today almost every driver in active use is Type 4. The other three explain why the spec is shaped the way it is, and Type 1 in particular still comes up in older codebases.

Type 1: JDBC-ODBC Bridge

A Type 1 driver translates JDBC calls into calls to an ODBC driver, which is a separate, non-Java database client API that predates JDBC. The bridge was useful in the late 1990s because almost every database already had an ODBC driver, so the JVM got "free" connectivity to any database with no Java-native driver. The cost was steep: you had to install the platform-specific ODBC driver on every machine, the bridge added an extra translation layer per call, and it was not thread-safe in any reasonable sense.

The JDK shipped its own sun.jdbc.odbc.JdbcOdbcDriver for years. It was removed in Java 8. Code that calls Class.forName("sun.jdbc.odbc.JdbcOdbcDriver") on a modern JDK will fail with ClassNotFoundException. Treat Type 1 as a piece of history; no new project should pick one.

Type 2: Native-API, Partly Java

A Type 2 driver uses a native (usually C or C++) client library installed on the machine, and calls into it through JNI. The Java side is a thin wrapper. Oracle's old OCI driver is the canonical example: the driver jar wraps Oracle's libclntsh shared library, which is what actually speaks Oracle's wire protocol.

Type 2 drivers can be fast because the native library is heavily optimized, but they bring all the problems of native code: you have to install the right library on every machine, version-match it to the server, deal with platform-specific binaries, and accept that a bug in the native code can crash the JVM. They are still used in a few specialized cases (large Oracle deployments, some mainframe gateways) but most teams have moved to other approaches.

Type 3: Network Protocol, All Java

A Type 3 driver talks to a middleware server in a database-agnostic protocol, and that middleware server talks to the actual database. The driver itself is pure Java, which makes it easy to deploy: just a jar, no native bits. The downside is the extra hop. Every query goes Java → middleware → database and back.

Type 3 had a moment in the early 2000s when firewalls were strict, databases were on private networks, and a single middleware server let many clients reach many databases through one common door. Today, application servers handle most of those cross-cutting concerns, and the extra hop is hard to justify. Type 3 drivers are rare in modern systems.

Type 4: Pure Java, Direct to Database

A Type 4 driver is written entirely in Java and speaks the database's native wire protocol directly. There is no native code, no middleware, no extra hop. The driver opens a TCP socket to the database, runs the database-specific handshake, and exchanges protocol-level messages over that socket. For in-memory databases like H2, the "wire protocol" is in-process method calls.

This is the modern default. The H2 driver, the PostgreSQL JDBC driver, MySQL Connector/J, Microsoft's mssql-jdbc, Oracle's ojdbc thin driver: all Type 4. Deployment is a jar on the classpath. Performance is good. Cross-platform behavior is uniform. When someone says "the JDBC driver" today, they almost always mean a Type 4 driver.

TypeImplementationJava code onlyNeeds native libsNeeds middlewareStatus today
1JDBC-to-ODBC bridgeNoYes (ODBC driver)NoRemoved from JDK in Java 8
2Native client via JNINo (partly)YesNoRare, legacy Oracle/mainframe
3Pure Java to middlewareYesNoYesMostly historical
4Pure Java direct wire protocolYesNoNoStandard choice today

In practice, "pick a JDBC driver" means "pick the Type 4 driver for your database." The rest is detail. Older Java textbooks walk through all four types in equal depth because they document the historical landscape; new projects use Type 4.

JDBC URL Anatomy

Every JDBC connection starts with a URL. The URL tells DriverManager two things: which driver to use, and where the database is. The format is fixed by the spec:

The jdbc: prefix is the same for every JDBC URL. The <subprotocol> identifies the database vendor: h2, postgresql, mysql, oracle, sqlite, sqlserver. The <subname> is whatever the driver needs to locate and connect to the database: a host name and port, a file path, a memory store, plus optional parameters.

DatabaseExample URLNotes
H2 (in-memory)jdbc:h2:mem:shop;DB_CLOSE_DELAY=-1mem: keeps the DB in process memory; DB_CLOSE_DELAY=-1 keeps it alive after all connections close.
H2 (file)jdbc:h2:./data/shopStores the database on disk under ./data/shop.mv.db.
PostgreSQLjdbc:postgresql://db.example.com:5432/shopStandard host:port/database form.
MySQLjdbc:mysql://localhost:3306/shop?useSSL=falseParameters use a query-string syntax.
SQLitejdbc:sqlite:./data/shop.dbFile path; no host or port.
Oraclejdbc:oracle:thin:@//db.example.com:1521/SHOPPDBThe thin keyword selects the Type 4 driver; the @// is part of Oracle's syntax.
SQL Serverjdbc:sqlserver://localhost:1433;databaseName=shopParameters separated by ; instead of &.

The format past <subprotocol> is entirely up to the driver. PostgreSQL uses a URL-like //host:port/db?params, MySQL uses the same with ?-and-& parameters, SQL Server uses ;-separated key-value pairs, Oracle has its own @//host:port/service shape. There is no single rule. Every driver's docs page lists the exact URL format it accepts, and getting that wrong is a common first-day JDBC error.

A runnable example that opens an H2 in-memory database, inserts a couple of products, and reads them back. The rest of the lesson uses this same pattern, since H2 needs no setup beyond the driver jar.

The URL jdbc:h2:mem:shop;DB_CLOSE_DELAY=-1 says "H2 driver, in-memory database named shop, keep the database alive even after the last connection closes." The DriverManager.getConnection call asks the driver to open the database; H2 returns a Connection that talks to an in-process data store. No TCP, no installation, no extra setup. This makes H2 convenient for examples and tests.

Parsing the URL is cheap, but the driver-selection step iterates every registered driver and asks each one acceptsURL(url). With a single driver on the classpath this is invisible. With ten drivers it still finishes in microseconds. The cost that actually matters is the connection handshake the driver does after URL parsing, which is usually one or more network round trips.

How Drivers Get Loaded

The JVM has to know about a driver class before DriverManager can use it. There are two mechanisms for that; the second is the standard today.

The Old Way: Class.forName

Before Java 6, you had to manually load the driver class by name:

The Class.forName call asks the JVM to find and initialize the class with that exact name. JDBC drivers were written to take advantage of class-initialization side effects: every driver's static initializer would call DriverManager.registerDriver(new ThisDriver()), which added an instance of the driver to DriverManager's internal list. After Class.forName returned, the driver was registered and getConnection could find it.

This pattern still appears in tutorials and in any Java code older than about 2010. It still works on modern JDKs because every modern driver keeps the static initializer for backward compatibility. But the call is no longer required, and writing it now is a small code smell.

The Modern Way: Service Provider Interface

Since JDBC 4 (which shipped with Java 6), DriverManager uses the Java Service Provider Interface (SPI) to discover drivers automatically. When the DriverManager class is initialized, it scans every jar on the classpath looking for a special file:

If a jar contains that file, the file's contents are interpreted as a list of class names, one per line, that implement java.sql.Driver. DriverManager loads each of those classes, instantiates them, and registers them. By the time your code calls DriverManager.getConnection, every driver on the classpath is already registered. No Class.forName call needed.

Inside the H2 driver jar, that file looks like this:

One line, one class name. Inside the PostgreSQL driver jar, the same file contains org.postgresql.Driver. MySQL Connector/J's contains com.mysql.cj.jdbc.Driver. Every modern JDBC driver ships this file, which enables the "just put the jar on the classpath" experience.

The diagram traces what happens the first time code touches DriverManager. The JVM hits the class for the first time, the class loads, and as part of its initialization it asks java.util.ServiceLoader to find every java.sql.Driver declared in META-INF/services. Each declared class is loaded; its static initializer registers a driver instance with DriverManager. By the time the JVM returns from the static initializer, every driver on the classpath has put itself on the list.

The loaded drivers can be listed at runtime. DriverManager.getDrivers() returns an enumeration over every registered driver. Run this with the H2 jar on the classpath and one entry appears; add another driver jar and more entries appear.

Output (with only the H2 jar on the classpath):

If nothing prints, no driver was registered. The fix is almost always "the driver jar is not on the classpath."

DriverManager Internals

DriverManager is the matchmaker. It holds a registered list of java.sql.Driver instances, and on a getConnection(url, ...) call, it walks the list asking each driver "do you handle this URL?" The first driver that says yes produces the Connection. If no driver matches, the call fails with SQLException: No suitable driver found for ....

That "do you handle this URL?" check is the acceptsURL method on java.sql.Driver. Every driver implements it. The H2 driver returns true for any URL that starts with jdbc:h2:. The PostgreSQL driver returns true for jdbc:postgresql:. MySQL handles jdbc:mysql:. The subprotocol is the discriminator.

Here is a small program that simulates what DriverManager.getConnection does internally. It iterates every registered driver and asks each one whether it accepts a few sample URLs.

Output (with only the H2 jar on the classpath):

Add the PostgreSQL driver jar to the classpath and the second line changes to accepted by org.postgresql.Driver. That is the entire selection algorithm: linear scan, first match wins, no central registry of subprotocols. Each driver vouches for itself.

The "first match wins" rule has a consequence. If two drivers on the classpath both accept the same subprotocol, the one that was registered first wins. This rarely matters in practice because subprotocols are unique to vendors, but it can come up if you have, for example, two versions of the same driver on the classpath: only one will be used, and which one depends on classpath order.

A getConnection call iterates every registered driver, calls acceptsURL on each, then on the first match calls connect. With N drivers the URL lookup is O(N) per getConnection. N is small, so this is invisible. The real cost is what happens after the match: opening a TCP connection and authenticating, which can take tens of milliseconds even on a fast network. Connection pools exist for that reason.

DriverManager.getConnection itself is overloaded with three signatures: (url), (url, user, password), and (url, java.util.Properties). All three end up calling the chosen driver's connect(url, properties) method.

Putting the Driver Jar on the Classpath

A JDBC driver is a regular Java library. Using one requires its jar on the classpath at both compile time and runtime. The compile-time part rarely matters for application code that only references java.sql.* (which lives in java.base), but it matters for code that imports driver-specific classes (rare, and usually a smell).

A small program saved as Catalog.java:

To compile it, you only need the JDK; java.sql is part of the standard library:

To run it, you need both your compiled class and the H2 driver jar on the classpath. On macOS and Linux the separator is :; on Windows it is ;.

Omitting the driver jar from the runtime classpath causes the code to compile fine but fail at the getConnection line with SQLException: No suitable driver found for jdbc:h2:.... The compile step did not need the driver because the code only imports java.sql.*. The runtime step needs the driver because DriverManager scans for META-INF/services/java.sql.Driver entries and registers the implementations at runtime.

Three common ways the jar ends up on the classpath:

  • Command line. java -cp app.jar:h2.jar MainClass. Fine for examples and scripts.
  • Build tool. Maven or Gradle declares a dependency on the driver, and the build tool puts the jar where it needs to go. This is the norm for every real project.
  • Application server. Tomcat, WildFly, or another container ships drivers in a shared lib directory so all deployed apps can use them.

The build-tool case is the standard in practice. A Maven pom.xml snippet looks like this:

A Gradle equivalent:

Either way, the build pulls the jar from a repository and adds it to the classpath. No Class.forName, no manual -cp flag, no SPI configuration. The driver shows up registered when your program starts.

Adding a driver jar adds a few hundred kilobytes to your deployment and a small amount of startup work the first time DriverManager is touched. Both are negligible. What matters is keeping the version current; old drivers can have security issues or speak outdated wire protocols that newer database servers reject.

A Worked End-to-End Example

To pull the lesson together, a program that lists registered drivers, opens an H2 connection, inserts a couple of customer rows, and reads them back. Every step exercises something the lesson covered: SPI registration, URL parsing, acceptsURL, the driver returning a real Connection.

The first block uses DriverManager.getDrivers() to show that the H2 driver was discovered automatically through SPI. The getConnection call iterates that same list, finds org.h2.Driver.acceptsURL("jdbc:h2:mem:shop...") returning true, and asks the H2 driver to open the connection. After that, conn.getMetaData().getDriverName() lets the driver report its name, which is a useful sanity check when multiple drivers are on the classpath and the code needs to confirm which one served a given URL.

Two practical takeaways from this example. First, the code never mentions org.h2.Driver; the URL does all the work. Second, the only database-specific piece of the program is the URL string. Swap jdbc:h2:mem:shop;DB_CLOSE_DELAY=-1 for jdbc:postgresql://db.example.com:5432/shop, swap the H2 jar for the PostgreSQL jar, change the credentials, and the rest of the code keeps working. That portability is what the JDBC layer provides, and it is why "pick the right driver" is mostly about picking the right URL.

What's Wrong With This Code?

Two driver-related bugs are common in early JDBC code.

Wrong driver class name:

The error is java.lang.ClassNotFoundException: org.h2.Drivers at the Class.forName line. The fix is to use the exact class name from the driver's documentation, or delete the Class.forName call since modern JDBC drivers register themselves via SPI:

Wrong subprotocol in the URL:

This fails with SQLException: No suitable driver found for jdbc:postgresql://localhost/shop. The H2 driver's acceptsURL returns false because the URL does not start with jdbc:h2:, and no other driver is registered. The fix is to either correct the URL to jdbc:h2:... or add the PostgreSQL driver jar to the classpath.

"No suitable driver found" is almost always either a typo or a missing jar. Either case is cheap to debug by checking DriverManager.getDrivers() first; printing it reveals whether the right driver is registered.

Quiz

JDBC Drivers Quiz

10 quizzes