How to Connect to SQLite from Java
SQLite has no server, so "connecting" from Java means opening a file. The standard way is the Xerial sqlite-jdbc driver, which bundles a native SQLite build for every common platform inside the JAR. You add one dependency, point a JDBC URL at a file, and you are querying. This guide covers the basic connection, file versus in-memory databases, the locking behavior that surprises everyone, and pooling.
The driver
| Library | Layer | When to use |
|---|---|---|
Xerial sqlite-jdbc (org.xerial:sqlite-jdbc) | JDBC driver | Almost always. The de facto standard, bundles native SQLite. |
| Hibernate / JPA | ORM | Entity mapping. Still uses sqlite-jdbc underneath. |
| HikariCP | Connection pool | Optional. Useful for read concurrency in WAL mode (see below). |
There is effectively one driver. Xerial wraps the real SQLite C library through JNI and ships precompiled binaries for Windows, macOS (Intel and Apple Silicon), and Linux (glibc and musl), so you do not need SQLite installed on the machine.
Adding the dependency
Maven:
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.53.2.0</version>
</dependency>Gradle:
implementation 'org.xerial:sqlite-jdbc:3.53.2.0'The version scheme tracks the embedded SQLite version: 3.53.2.0 bundles SQLite 3.53.2, with the trailing digit being the driver's own release counter (as of June 2026). Modern versions self-register, so Class.forName("org.sqlite.JDBC") is no longer required; you will still see it in older tutorials.
The basic connection
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class Example {
public static void main(String[] args) throws Exception {
String url = "jdbc:sqlite:app.db";
try (Connection conn = DriverManager.getConnection(url)) {
String sql = "SELECT id, email FROM users WHERE created_at > ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, "2026-01-01");
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getLong("id") + " " + rs.getString("email"));
}
}
}
}
}
}Note that there is no username or password. SQLite has no authentication and no network layer; access control is whoever can read the file. The same try-with-resources, ? placeholder, and 1-based parameter rules apply as in any JDBC code.
Connection URL formats
jdbc:sqlite:app.db relative path (created if missing)
jdbc:sqlite:/var/data/app.db absolute path
jdbc:sqlite::memory: in-memory, dies when the connection closes
jdbc:sqlite: same as :memory: (empty path)
The accidental-creation trap: if you point a connection at a file that does not exist, SQLite creates an empty one rather than failing. A typo in the path silently gives you a fresh, empty database and a confusing "no such table" error later. To require an existing file, set the open_mode option or check Files.exists() before connecting.
In-memory databases
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) {
// schema and data live only as long as this connection
}Each :memory: connection is its own private database. Two connections to :memory: do not see the same data. If you need several connections to share one in-memory database (common in tests), use a shared-cache named in-memory URL:
jdbc:sqlite:file:testdb?mode=memory&cache=shared
WAL mode and concurrency
By default SQLite uses rollback-journal mode, where a writer blocks all readers. For anything with concurrent access, switch to Write-Ahead Logging so readers and one writer can proceed at the same time:
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:app.db");
var st = conn.createStatement()) {
st.execute("PRAGMA journal_mode=WAL");
st.execute("PRAGMA synchronous=NORMAL");
}WAL still allows only one writer at a time; it just stops that writer from blocking readers. The journal_mode setting is persistent (stored in the database file), but synchronous is per-connection, so set it on every connection.
The "database is locked" error
This is the single most common SQLite problem in Java. SQLite serializes writes, and if a second connection tries to write while the first holds the write lock, you get SQLITE_BUSY: database is locked immediately. The fix is a busy timeout, which tells SQLite to retry for a while before giving up:
import org.sqlite.SQLiteConfig;
SQLiteConfig config = new SQLiteConfig();
config.setBusyTimeout(5000); // milliseconds
try (Connection conn = config.createConnection("jdbc:sqlite:app.db")) {
// ...
}Even with a busy timeout, a long-running write transaction can starve others. Keep write transactions short, and do not hold a transaction open while doing slow work in Java.
Transactions
conn.setAutoCommit(false);
try (PreparedStatement ps = conn.prepareStatement(
"INSERT INTO events (user_id, kind) VALUES (?, ?)")) {
for (Event e : events) {
ps.setLong(1, e.userId());
ps.setString(2, e.kind());
ps.addBatch();
}
ps.executeBatch();
conn.commit();
} catch (Exception e) {
conn.rollback();
throw e;
}Wrapping many inserts in one transaction is the biggest single performance win in SQLite. Each implicit-autocommit insert forces an fsync; batching thousands of rows into one transaction turns thousands of fsyncs into one.
Pooling
SQLite does not need a pool the way a client-server database does, because there is no connection handshake or network round trip. But a small pool is still useful in WAL mode: keep a few read connections open and route writes through a single dedicated connection to avoid contention. With HikariCP:
HikariConfig hc = new HikariConfig();
hc.setJdbcUrl("jdbc:sqlite:app.db");
hc.setMaximumPoolSize(4);
hc.addDataSourceProperty("journal_mode", "WAL");Do not set a large pool size. More write connections will not help, because writes serialize regardless; they will just queue on the busy timeout.
Errors you will actually hit
| Error | Cause | Fix |
|---|---|---|
SQLITE_BUSY: database is locked | Concurrent write while another holds the lock | Set busy_timeout, use WAL, keep write transactions short |
no such table after a fresh start | Typo in path created an empty new database | Verify the path; SQLite silently creates missing files |
SQLITE_READONLY | File or directory not writable by the process | Fix filesystem permissions; check mode=ro is not set |
SQLITE_CANTOPEN | Directory in the path does not exist | SQLite creates the file but not parent directories; create them first |
out of memory on a large :memory: db | The whole database lives in RAM | Use a file-backed database for large data sets |
Limitations to be honest about
SQLite is a single-file embedded database, not a server. It has no built-in user accounts, no network access (you connect to a file on local or attached storage, not over TCP), and it serializes writes. It is excellent for application-local storage, tests, and read-heavy workloads, and a poor fit for many concurrent writers or multi-machine access. Those are properties of SQLite, not the Java driver.
For loading data into SQLite, see our import CSV to SQLite guide. For a desktop way to browse the file, see SQLite GUI clients.
Mako connects to SQLite, PostgreSQL, MySQL, and more, with AI-powered autocomplete for exploring your data. Try it free at mako.ai.
Skip the terminal. Use Mako.
Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.