Java SQLite Tutorial: Connect with JDBC, Insert, Query and Use Transactions (Tested)

JDBC looks the same for every database, but the JDK doesn’t include a driver for SQLite, so DriverManager.getConnection("jdbc:sqlite:contacts.db") fails with No suitable driver found until you add one. A single jar fixes that, with no server to install. In this article I’ll walk you through building a small contacts program in Java with SQLite, from the first connection to transactions.

TLDR

  • Add the org.xerial:sqlite-jdbc driver (3.53.4.0 here). It bundles SQLite itself, so there’s nothing else to install.
  • Connect with DriverManager.getConnection("jdbc:sqlite:contacts.db"). The file is created if it doesn’t exist.
  • Use PreparedStatement with ? placeholders for every value you insert or filter on.
  • For a transaction, turn off auto-commit with setAutoCommit(false). Then call commit() or rollback().
  • No suitable driver found for jdbc:sqlite:… means the driver jar isn’t on the classpath.

What is SQLite JDBC and what will we build?

Java talks to databases through JDBC, and the sqlite-jdbc driver (by Xerial) is the JDBC driver for SQLite. Its README says it “requires no configuration since native libraries for major OSs, including Windows, macOS, Linux etc., are assembled into a single JAR” (sqlite-jdbc README). The SQLite engine runs inside your Java process, and the database is one file.

To show you the full cycle, I will build ContactsApp.java. It creates a contacts table and adds two people. Then it runs a transaction that fails halfway and prints what’s left.

Prerequisites

RequirementVersion used here
JDKJava 21 (eclipse-temurin:21-jdk), which runs a single source file with java ContactsApp.java
SQLite JDBC driverorg.xerial:sqlite-jdbc:3.53.4.0, the latest release on Maven Central on 7 October 2026
Build tool (optional)Maven or Gradle, or the plain jar as below

Building a contacts program with Java and SQLite

Everything fits in one Java file plus the driver jar.

Step 1: Add the SQLite JDBC driver

With Maven, add the dependency to pom.xml (not executed here; I used the jar directly):

<dependency>
  <groupId>org.xerial</groupId>
  <artifactId>sqlite-jdbc</artifactId>
  <version>3.53.4.0</version>
</dependency>

With Gradle (not executed here):

implementation 'org.xerial:sqlite-jdbc:3.53.4.0'

Without a build tool, download the jar from Maven Central. That’s what I did for this test:

$ mkdir lib && curl -sSL -o lib/sqlite-jdbc-3.53.4.0.jar https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.53.4.0/sqlite-jdbc-3.53.4.0.jar && ls lib
sqlite-jdbc-3.53.4.0.jar

Step 2: Write the program

Save this as ContactsApp.java. Each block does one job, and the comments say which:

import java.sql.*;

public class ContactsApp {
    public static void main(String[] args) throws SQLException {
        // Creates contacts.db in the current folder if it doesn't exist.
        try (Connection conn = DriverManager.getConnection("jdbc:sqlite:contacts.db")) {
            System.out.println("SQLite version: " + conn.getMetaData().getDatabaseProductVersion());

            try (Statement st = conn.createStatement()) {
                st.execute("CREATE TABLE IF NOT EXISTS contacts ("
                        + "id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE)");
            }

            // Insert with a PreparedStatement: values are bound, never pasted into the SQL.
            String insert = "INSERT INTO contacts (name, email) VALUES (?, ?)";
            try (PreparedStatement ps = conn.prepareStatement(insert, Statement.RETURN_GENERATED_KEYS)) {
                for (String[] c : new String[][] {{"Ada", "ada@example.com"}, {"Linus", "linus@example.com"}}) {
                    ps.setString(1, c[0]);
                    ps.setString(2, c[1]);
                    ps.executeUpdate();
                    try (ResultSet keys = ps.getGeneratedKeys()) {
                        keys.next();
                        System.out.println("Inserted " + c[0] + " with id " + keys.getLong(1));
                    }
                }
            }

            // Update and delete return the number of rows changed.
            try (PreparedStatement ps = conn.prepareStatement("UPDATE contacts SET email = ? WHERE name = ?")) {
                ps.setString(1, "ada@lovelace.dev");
                ps.setString(2, "Ada");
                System.out.println("Updated rows: " + ps.executeUpdate());
            }

            // A transaction: both inserts or neither.
            conn.setAutoCommit(false);
            try (PreparedStatement ps = conn.prepareStatement(insert)) {
                ps.setString(1, "Grace");
                ps.setString(2, "grace@example.com");
                ps.executeUpdate();
                ps.setString(1, "Grace again");
                ps.setString(2, "grace@example.com"); // violates UNIQUE
                ps.executeUpdate();
                conn.commit();
            } catch (SQLException e) {
                conn.rollback();
                System.out.println("Rolled back: " + e.getMessage());
            } finally {
                conn.setAutoCommit(true);
            }

            try (Statement st = conn.createStatement();
                 ResultSet rs = st.executeQuery("SELECT id, name, email FROM contacts ORDER BY id")) {
                while (rs.next()) {
                    System.out.println(rs.getInt("id") + " | " + rs.getString("name") + " | " + rs.getString("email"));
                }
            }
        }
    }
}

What to notice in the code:

  • The connection string jdbc:sqlite:contacts.db opens the file, or creates it, relative to the folder you run from. Use a full path to put it elsewhere.
  • try-with-resources closes every JDBC object for you.
  • RETURN_GENERATED_KEYS gives you the new row’s id straight after the insert.
  • The transaction inserts Grace twice. The second insert breaks the UNIQUE rule on email, so rollback() undoes the first insert too.

Running it

Run the file with the driver on the classpath:

$ java -cp lib/sqlite-jdbc-3.53.4.0.jar ContactsApp.java
SQLite version: 3.53.4
Inserted Ada with id 1
Inserted Linus with id 2
Updated rows: 1
Rolled back: [SQLITE_CONSTRAINT_UNIQUE] A UNIQUE constraint failed (UNIQUE constraint failed: contacts.email)
1 | Ada | ada@lovelace.dev
2 | Linus | linus@example.com

Grace isn’t in the final list, because the rollback removed her first insert along with the failed one. The program created contacts.db in the working folder:

$ ls contacts.db
contacts.db

Common problems

  • SQLITE_CONSTRAINT_UNIQUE on a second run: the program inserts the same emails every time, so running it again on the same file fails on the first insert:
$ java -cp lib/sqlite-jdbc-3.53.4.0.jar ContactsApp.java 2>&1 | grep -m1 SQLITE_CONSTRAINT
Exception in thread "main" org.sqlite.SQLiteException: [SQLITE_CONSTRAINT_UNIQUE] A UNIQUE constraint failed (UNIQUE constraint failed: contacts.email)

Delete contacts.db between runs, or use INSERT OR IGNORE. See SQLite UNIQUE constraint.

  • No suitable driver found for jdbc:sqlite:contacts.db: the driver jar isn’t on the classpath. Here’s the same program run without it:
$ java ContactsApp.java 2>&1 | head -1
Exception in thread "main" java.sql.SQLException: No suitable driver found for jdbc:sqlite:contacts.db

Add the dependency or the -cp flag. If you build a fat jar with the Maven Shade plugin, the README notes you may need a shade transformer to fix this error.

  • database is locked: another connection is writing. Keep one connection per thread and close it when you’re done. See database is locked.

Conclusion

SQLite in Java needs one dependency and a jdbc:sqlite: connection string. Use PreparedStatement for values and try-with-resources for cleanup. Turn off auto-commit when several writes must succeed together.

Further reading

Tested with sqlite-jdbc 3.53.4.0 (SQLite 3.53.4) on OpenJDK 21.0.12 on 7 October 2026.