Driver FixRecommendedSound, Wi-Fi or graphics acting up? Check drivers firstFind missing or outdated drivers fast.Check DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run Scan×
Skip to content
World desk9 min

Build a Java Hotel Reservation System with JDBC and MySQL (Without Double Bookings)

A step-by-step build of a Java and MySQL hotel booking core: JDBC connection, prepared statements, and a transaction with row locking that stops double bookings.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A hotel reservation app in Java needs three things: a JDBC connection to MySQL, parameterized SQL for every guest-supplied value, and a booking write that stays correct when two people reserve the same room at the same moment. The first two are routine. The third is where most tutorials fail, because a plain availability SELECT followed later by an INSERT does not stop competing bookings. This walkthrough builds the whole thing as an illustrative design. The schema, statuses and date rules below are choices made for this article, not a reconstruction of any particular project.

What you need and which versions

JDBC is Java’s database API. MySQL Connector/J is the JDBC driver that talks to MySQL. Oracle’s JDBC tutorial names the driver class com.mysql.cj.jdbc.Driver and shows the URL form jdbc:mysql://host:port/database. The tutorial says DataSource is the preferred way to obtain connections, though its simpler examples use DriverManager. The tutorial was written for JDK 8 and does not cover later Java improvements. Treat it as a source for stable JDBC concepts, and check current product docs for setup details.

As an Amazon Associate I earn from qualifying purchases.

The official Connector/J guide, in a revision dated 2026-08-31, describes Connector/J 26.7. It recommends that version for production and says it targets MySQL Server 8.0 and up. Pin whichever driver and server versions you actually use, and record them in your build file.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • A current JDK (the examples use text blocks, so Java 15 or later)
  • MySQL Server 8.0 or later with InnoDB tables
  • Connector/J added as a Maven or Gradle dependency

Step 1: Design a small schema

The design needs room inventory, guest details and reservations. This version tracks individual rooms, which makes locking straightforward. It uses a half-open stay convention: a guest occupies the nights from check_in up to, but not including, check_out. Someone leaving on the 10th and someone arriving on the 10th therefore do not conflict.

CREATE TABLE rooms (
  id          INT AUTO_INCREMENT PRIMARY KEY,
  room_number VARCHAR(10) NOT NULL UNIQUE,
  room_type   VARCHAR(30) NOT NULL,
  nightly_rate DECIMAL(10,2) NOT NULL
) ENGINE=InnoDB;

CREATE TABLE guests (
  id    INT AUTO_INCREMENT PRIMARY KEY,
  name  VARCHAR(100) NOT NULL,
  email VARCHAR(150) NOT NULL UNIQUE
) ENGINE=InnoDB;

CREATE TABLE reservations (
  id        BIGINT AUTO_INCREMENT PRIMARY KEY,
  room_id   INT NOT NULL,
  guest_id  INT NOT NULL,
  check_in  DATE NOT NULL,
  check_out DATE NOT NULL,
  status    ENUM('CONFIRMED','CANCELLED') NOT NULL DEFAULT 'CONFIRMED',
  CONSTRAINT fk_res_room  FOREIGN KEY (room_id)  REFERENCES rooms(id),
  CONSTRAINT fk_res_guest FOREIGN KEY (guest_id) REFERENCES guests(id),
  CONSTRAINT chk_dates CHECK (check_out > check_in),
  INDEX idx_room_dates (room_id, check_in, check_out)
) ENGINE=InnoDB;

Two decisions here are yours to make for any real system. The first is which statuses consume inventory. Here only CONFIRMED does, and cancelled rows are ignored. If you add pending holds, include them in the overlap check. The second is the index on (room_id, check_in, check_out). MySQL advises indexing the columns used by locking reads and updates, and this index serves the overlap check below.

Step 2: Connect with a DataSource

DriverManager is fine for a throwaway script. A DataSource gives you centralized configuration and a place to plug in connection pooling, so it is the better default for an application. Keep credentials out of source. Oracle states plainly that its sample code does not use deployed password-management techniques, so do not copy embedded sample passwords into anything you deploy.

import com.mysql.cj.jdbc.MysqlDataSource;
import javax.sql.DataSource;

public final class Db {
  public static DataSource create() {
    MysqlDataSource ds = new MysqlDataSource();
    ds.setUrl(System.getenv("HOTEL_DB_URL")); // e.g. jdbc:mysql://localhost:3306/hotel
    ds.setUser(System.getenv("HOTEL_DB_USER"));
    ds.setPassword(System.getenv("HOTEL_DB_PASSWORD"));
    return ds;
  }
}

Put the database name in the URL. If you must switch databases on an open connection, Connector/J’s URL documentation says to use Connection.setCatalog() instead of the SQL USE statement. For production traffic, wrap or replace this with a pooled DataSource so each request does not open a fresh connection.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Step 3: Use prepared statements for every user value

Guest names, emails, room identifiers and dates all arrive from outside your code. Oracle’s tutorial puts the reason in one sentence: “Prepared statements always treat client-supplied data as content of a parameter and never as a part of an SQL statement.” Never build SQL by concatenating these values.

public int findOrCreateGuest(Connection c, String name, String email) throws SQLException {
  try (PreparedStatement ps = c.prepareStatement(
      "SELECT id FROM guests WHERE email = ?")) {
    ps.setString(1, email);
    try (ResultSet rs = ps.executeQuery()) {
      if (rs.next()) return rs.getInt(1);
    }
  }
  try (PreparedStatement ps = c.prepareStatement(
      "INSERT INTO guests (name, email) VALUES (?, ?)",
      Statement.RETURN_GENERATED_KEYS)) {
    ps.setString(1, name);
    ps.setString(2, email);
    ps.executeUpdate();
    try (ResultSet keys = ps.getGeneratedKeys()) {
      keys.next();
      return keys.getInt(1);
    }
  }
}

Pass dates with setObject(i, localDate) or setDate, and keep them as java.time.LocalDate in your own code. Note the lookup-then-insert above can itself race: two requests with the same new email could both miss and both insert. The UNIQUE constraint on email catches that, and you handle the duplicate-key error in Step 6.

Step 4: Search availability (a read, not a promise)

With the half-open convention, two stays overlap when the existing check-in is before the requested check-out and the existing check-out is after the requested check-in. A room is available when it has no overlapping confirmed reservation.

String sql = """
  SELECT r.id, r.room_number, r.nightly_rate
  FROM rooms r
  WHERE r.room_type = ?
    AND NOT EXISTS (
      SELECT 1 FROM reservations x
      WHERE x.room_id = r.id
        AND x.status = 'CONFIRMED'
        AND x.check_in  < ?   -- requested check_out
        AND x.check_out > ?   -- requested check_in
    )
  ORDER BY r.room_number
  """;

The result is only a snapshot. By the time the guest clicks Book, another session may have taken that room. Treat search results as suggestions and make the booking step re-verify under a lock.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Step 5: Confirm the booking inside a transaction with a lock

MySQL InnoDB’s default isolation level is REPEATABLE READ. Ordinary reads there are consistent snapshot reads, so two sessions can each see the room as free and each insert a reservation. Changing the isolation level alone does not fix this. MySQL’s documentation notes that under READ COMMITTED, gap locking for ordinary searches is disabled except for foreign-key and duplicate-key checks, so phantom rows can appear.

The fix is to give competing bookings something to queue on. MySQL documents SELECT ... FOR UPDATE as a locking read: it locks the index records it scans, and the locks are released at commit or rollback. In a per-room design the natural lock target is the room row. Every booking attempt for room 12 first locks room 12, then checks for overlap, then inserts. The second session waits at the lock and, once the first commits, sees the new reservation.

public long book(DataSource ds, int roomId, String name, String email,
                 LocalDate in, LocalDate out) throws SQLException, RoomUnavailableException {
  if (!out.isAfter(in)) throw new IllegalArgumentException("check-out must be after check-in");

  try (Connection c = ds.getConnection()) {
    c.setAutoCommit(false);
    try {
      // 1. Serialize bookings for this room
      try (PreparedStatement lock = c.prepareStatement(
          "SELECT id FROM rooms WHERE id = ? FOR UPDATE")) {
        lock.setInt(1, roomId);
        try (ResultSet rs = lock.executeQuery()) {
          if (!rs.next()) throw new IllegalArgumentException("No such room");
        }
      }

      // 2. Re-check overlap now that we hold the lock
      try (PreparedStatement chk = c.prepareStatement("""
          SELECT 1 FROM reservations
          WHERE room_id = ? AND status = 'CONFIRMED'
            AND check_in < ? AND check_out > ?
          LIMIT 1""")) {
        chk.setInt(1, roomId);
        chk.setObject(2, out);
        chk.setObject(3, in);
        try (ResultSet rs = chk.executeQuery()) {
          if (rs.next()) throw new RoomUnavailableException();
        }
      }

      // 3. Guest + reservation in the same transaction
      int guestId = findOrCreateGuest(c, name, email);
      long reservationId;
      try (PreparedStatement ins = c.prepareStatement("""
          INSERT INTO reservations (room_id, guest_id, check_in, check_out)
          VALUES (?, ?, ?, ?)""", Statement.RETURN_GENERATED_KEYS)) {
        ins.setInt(1, roomId);
        ins.setInt(2, guestId);
        ins.setObject(3, in);
        ins.setObject(4, out);
        ins.executeUpdate();
        try (ResultSet k = ins.getGeneratedKeys()) { k.next(); reservationId = k.getLong(1); }
      }

      c.commit();
      return reservationId;
    } catch (Exception e) {
      c.rollback();
      throw e;
    }
  }
}

Keep this transaction short. Do validation of user input, price calculation and any email sending outside it, so the room lock is held for as little time as possible. If a single method touches several tables, always touch them in the same order. MySQL recommends this to reduce deadlocks.

Choosing the lock target for your inventory model

The room-row lock works because the thing being sold is a specific, lockable row. Other models need a different coordination point.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Inventory model What must be coordinated Approach
Individual rooms (this article) One room’s date range Lock the room row with FOR UPDATE, re-check overlap, insert
Room-type capacity (“3 doubles left”) The count per type per night Keep a per-type, per-night inventory row and lock or decrement it for every night in the stay, in date order

The MySQL sources establish the locking mechanics, not a hotel schema, so which model fits depends on whether guests are assigned a specific room at booking or only at check-in. A room-type design with no lockable row for the type and night would reproduce the same race you are trying to remove.

Why not just use isolation levels or a unique constraint?

A unique key can stop two reservations that share exactly the same room and start date, but it cannot express “these date ranges overlap”. Raising isolation to SERIALIZABLE is possible, but it changes locking behavior for every query and tends to produce more lock waits and deadlocks. An explicit, narrow lock on the row you are allocating is easier to reason about.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Step 6: Handle errors visibly

Translate database failures into messages a guest can act on, and retry only what is safe to retry.

  • Room taken (your own RoomUnavailableException): show “That room was just booked. Here are the other available rooms for those dates” and rerun the search.
  • Duplicate key (SQLState 23000, MySQL error 1062): typically a guest email inserted by a concurrent request. Re-run the lookup once, or report that the email is already registered.
  • Deadlock (SQLState 40001, MySQL error 1213): InnoDB chose your transaction as the victim and rolled it back. MySQL’s guidance is that deadlocks can still happen even with good design, so the application must be prepared. Retry the whole transaction a small, bounded number of times.
  • Lock wait timeout (error 1205): another session held the room lock too long. Fail with a “try again” message instead of retrying forever.
for (int attempt = 1; ; attempt++) {
  try {
    return book(ds, roomId, name, email, in, out);
  } catch (SQLException e) {
    boolean deadlock = "40001".equals(e.getSQLState());
    if (!deadlock || attempt >= 3) throw e;
  }
}

Never swallow exceptions after a rollback. Log the SQLState and error code so you can tell a deadlock from a constraint failure later.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Step 7: Cancellation and verification

Cancel by setting status = 'CANCELLED' inside a transaction rather than deleting the row, which keeps a history and frees the room under the overlap rule above. Use the same room lock if cancellation and rebooking can interleave.

To verify the locking actually works, run a test with two threads, each with its own connection, calling book() for the same room and overlapping dates at the same time. Exactly one should return a reservation ID and the other should throw RoomUnavailableException. Then remove the FOR UPDATE step and rerun it many times. Without the lock you may see both succeed, which demonstrates the race. Whether and how often it appears depends on timing, so loop the test rather than relying on one run.

Before you call it production-ready

  • Read credentials from environment variables or a secrets manager, never from source.
  • Use a pooled DataSource.
  • Decide and document the date convention, the statuses that consume inventory, the cancellation rules and the room-allocation policy. None of these come from JDBC or MySQL.
  • Add payment, authentication and an interface separately. This design covers only the data and booking core.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.