Transactions in Web Apps
Transactions in Web Apps
A database transaction is a sequence of SQL statements that the database engine treats as a single, indivisible unit of work. Either every statement in the group succeeds and is permanently recorded — a commit — or none of them take effect and the database is restored to the state it was in before the first statement ran — a rollback. This guarantee, codified in the ACID properties, is what separates a reliable web application from one that leaves data in an inconsistent state under error conditions.
Why Every Non-Trivial Write Operation Needs a Transaction
Consider an e-commerce order placement: you must insert a row into orders, insert several rows into order_items, and decrement stock in products. If the stock update fails after the order was already written, your database contains a phantom order with no inventory deducted. Without a transaction, partial writes produce corrupt business data that is often invisible until an audit.
JDBC Default: Auto-Commit Mode
By default, every JDBC Connection starts in auto-commit mode: each statement is its own micro-transaction, committed immediately after execution. This is fine for reads, but it means multi-step writes have no protection at all. The first thing you do when you need a real transaction is turn auto-commit off:
From that point, statements accumulate in an open transaction until you explicitly call commit() or rollback(). Calling close() on a connection that still has an open, uncommitted transaction will cause the driver to roll back automatically — but relying on that is a bug, not a feature.
The Canonical Transaction Pattern in a Servlet / Service Method
The gold-standard structure for a transactional operation using plain JDBC looks like this:
The structure is deliberate: setAutoCommit(false) is called first, then every statement runs, and a catch at the same level as the try block calls rollback() before re-throwing. The outer try-with-resources closes the connection regardless of success or failure.
Treating a Servlet Request as a Unit of Work
In a web application, the natural boundary for a transaction is usually a single HTTP request. The pattern is: open a connection, begin a transaction, do all the database work the request requires, commit if everything succeeded, roll back if anything threw, then return the connection to the pool. This is sometimes called the Unit of Work pattern.
A clean way to implement this without scattering setAutoCommit / commit / rollback calls throughout your DAO layer is to push the transaction boundary into a helper method that accepts a lambda:
Callers pass a lambda that receives the already-open, already-transactional connection. The rollback logic lives in exactly one place:
Savepoints — Partial Rollbacks
Sometimes you want to undo only part of a transaction — for example, to retry a failed sub-operation without losing earlier successful writes. JDBC supports savepoints:
Not every database or driver supports savepoints equally well; verify your target DB's documentation before relying on them in critical paths.
Isolation Levels — A Brief Map
JDBC lets you set the isolation level per connection with conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ). The four standard levels from weakest to strongest are:
- READ_UNCOMMITTED — can read another transaction's uncommitted (dirty) data. Almost never appropriate.
- READ_COMMITTED — reads only committed data; the default in PostgreSQL and SQL Server. Prevents dirty reads.
- REPEATABLE_READ — re-reading a row in the same transaction always yields the same result; the MySQL/InnoDB default. Prevents non-repeatable reads.
- SERIALIZABLE — full serial execution semantics; prevents phantom reads but can be slow under concurrency.
cfg.setTransactionIsolation("TRANSACTION_READ_COMMITTED") — set it once there rather than per-connection.
Propagating Connections Through the DAO Layer
A common beginner mistake is to have each DAO method call dataSource.getConnection() independently. When you do that, every DAO call executes in its own separate connection — and therefore in its own separate transaction. Calls across DAOs can never be atomic. The solution is to pass the connection into DAO methods from the service layer, where the transaction is opened:
Summary
Every multi-step database write in a web application belongs inside a transaction. The JDBC pattern is: call setAutoCommit(false), execute your statements, call commit() on success, and rollback() in the catch block before re-throwing. Encapsulate this in a reusable utility (a Tx.run() helper or, later, a Spring @Transactional annotation) so the mechanics live in one place. Pass the open connection into your DAO methods rather than having them open their own — that is the only way multiple DAO calls can participate in the same transaction. In the next lesson you will look at how to handle SQLException and resource leaks gracefully across the entire JDBC call stack.