How to Explain It in an Interview
A transaction is a group of database operations that commits as one unit or rolls back as one unit. Concurrent PHP requests normally use separate database sessions and may run transactions at the same time. The isolation level determines which effects of other transactions are visible and how the database handles certain conflicts.
The SQL standard defines minimum guarantees for isolation levels, but exact behavior varies by database engine. Some engines mainly use locks, some use multi-version concurrency control, and many use both. Therefore, I would identify the database before relying on implementation-specific behavior.
Dirty reads
A dirty read occurs when one transaction reads a value written by another transaction that has not committed. If the writer later rolls back, the reader used a value that never became permanent.
Under the SQL standard, READ UNCOMMITTED may permit dirty reads. Some database engines still prevent true dirty reads at this level or treat it like READ COMMITTED. Dirty reads are rarely acceptable for balances, inventory, permissions, order states, or other business decisions.
Non-repeatable reads
A non-repeatable read occurs when one transaction reads the same row twice and receives different committed values because another transaction updated or deleted that row between the reads.
At READ COMMITTED, each statement commonly sees data committed before that statement begins. A PHP transaction can therefore read one value and later receive a different value for the same row.
This may be acceptable for short independent queries. It is unsafe when application logic assumes an earlier value remains unchanged before making a later decision or write.
Phantom reads
A phantom read occurs when one transaction repeats a condition-based query and receives a different set of matching rows because another transaction inserted, deleted, or changed rows between the two queries.
For example, one request counts pending orders twice while another request inserts a new pending order. At an isolation level that permits phantoms, the second query may contain an additional row.
The SQL standard requires phantom prevention at SERIALIZABLE. Practical behavior at REPEATABLE READ differs among engines. Snapshot-based ordinary reads may remain stable, while locking reads, writes, and predicate protection may follow different rules.
Write conflicts and lost updates
Read anomalies are not the only concern. Two PHP requests may update the same row, reserve the same item, or enforce a rule involving several rows.
A direct write conflict may cause one transaction to wait, deadlock, receive a serialization failure, or fail with another database-specific error. An unsafe read-modify-write sequence may also produce a lost update, where one request overwrites another request's result.
For example:
1. Request A reads stock as 10. 2. Request B reads stock as 10. 3. Both calculate a new value independently. 4. Both write their calculated value.
Depending on the SQL and database behavior, one write may overwrite the effect of the other.
A safer approach is an atomic conditional update that reduces stock only when enough stock remains, followed by checking the affected-row count. Other protections include row locking, optimistic version checks, and database constraints.
Write skew
Snapshot-based isolation can allow write skew even when each transaction sees a stable snapshot. Two transactions read the same multi-row condition, update different rows, and together violate a business rule.
For example, two requests both see that two staff members are on call. Each request independently removes a different staff member, leaving nobody on call. Because the transactions update different rows, a simple same-row write conflict may not occur.
A serializable transaction, targeted locking, an enforceable constraint, or a redesigned data model may be required for this rule.
Common isolation levels
READ UNCOMMITTED
This is the weakest standard level. Dirty reads, non-repeatable reads, and phantom reads may occur under the standard model.
It is rarely appropriate for PHP business workflows. The exact database implementation still matters because some engines provide stronger behavior than the standard minimum.
READ COMMITTED
Dirty reads are prevented. Separate statements in the same transaction may observe newly committed changes, so non-repeatable reads and phantom reads may occur.
This is a practical default for many short web transactions because it usually provides good concurrency. Application code must not assume that an earlier read remains current. Important writes should use atomic SQL, constraints, explicit locks, or optimistic concurrency checks.
REPEATABLE READ
The SQL standard prevents dirty reads and non-repeatable reads at this level, but it does not require the same phantom protection as SERIALIZABLE. Actual implementations differ substantially.
A multi-version database may provide one stable snapshot for ordinary reads. That does not automatically prevent every lost update, write skew, deadlock, or conflicting write. Locking reads may also behave differently from ordinary snapshot reads.
This level is useful when several related reads should use one consistent view, but the application must understand whether that view can become stale relative to concurrent commits.
SERIALIZABLE
SERIALIZABLE provides the strongest standard isolation. The committed result must be equivalent to transactions running in some valid serial order, even if the database executes them concurrently.
A database may enforce this by blocking operations, using row, range, or predicate locks, detecting dangerous dependency patterns, or aborting a transaction. Serializable isolation therefore does not mean every request succeeds immediately. PHP code must be prepared for deadlocks or serialization failures and may need to retry the complete transaction.
Choosing the practical level
For ordinary short create, read, update, and delete requests, READ COMMITTED is often a reasonable starting point when combined with atomic SQL and database constraints.
Use REPEATABLE READ when several related reads must use a stable view and the selected database's exact semantics fit the workflow.
Use SERIALIZABLE when correctness depends on a multi-row or predicate-based rule that cannot be protected reliably with a simpler atomic statement, constraint, optimistic version check, or targeted lock.
Do not automatically choose the strongest level for every request. Stronger isolation can increase waiting, aborted transactions, lock contention, retained row versions, cleanup work, and retry cost. In lock-based systems it can also increase deadlock opportunities. Under high contention, it may reduce throughput.
Do not choose weaker isolation only for speed. Incorrect inventory, duplicate redemption, or invalid account state may cost far more than a properly designed transaction.
Database protections that complement isolation
Isolation should be combined with features that express the business rule directly:
- Use atomic conditional updates for counters, balances, and inventory changes.
- Use unique constraints to prevent duplicate identifiers or one-time claims.
- Use foreign-key and check constraints for relationships and row-level rules.
- Use SELECT ... FOR UPDATE or the database's equivalent when specific rows must be locked before related writes.
- Use a version column or expected previous value for optimistic concurrency.
- Use indexes that support important search and locking predicates.
Prepared statements and parameter binding protect data values from SQL injection. They do not provide transaction isolation and do not prevent concurrency anomalies. Dynamic identifiers cannot be made safe merely by binding them as parameters; they must be selected from a trusted allowlist and quoted using database-specific rules when necessary.
PHP and PDO responsibilities
PDO does not define one universal isolation implementation. The selected PDO driver sends transaction and SQL commands to the database, and the database engine supplies the behavior.
A PHP request should:
- Use the same PDO connection for the entire transaction.
- Configure the required isolation level using syntax supported by that database and at the time required by that database.
- Begin the transaction immediately before the protected database work.
- Keep HTTP calls, file operations, user interaction, and slow computation outside the transaction.
- Commit only after every required statement succeeds.
- Roll back after an exception or validation failure.
- Recognize retryable failures through database-specific SQLSTATE values or driver error codes.
- Retry the entire transaction, not only the failed statement.
- Use a small retry limit and backoff to avoid retry storms.
- Prevent external side effects from being duplicated during retries.
PDO::beginTransaction() starts a transaction but does not itself select the desired isolation level. Isolation must be configured according to the database's supported commands and connection rules.
A retry must re-run all reads and writes because the database state may have changed. Retrying only the failed statement can make it inconsistent with decisions based on earlier reads.
Connection lifecycle
Isolation settings can be transaction-scoped or session-scoped depending on the database and command used. Persistent PDO connections, long-running PHP workers, connection pools, and database proxies may reuse a session. Session-level changes can therefore affect later work if they are not reset.
Traditional request-based PHP deployments often release ordinary non-persistent connections at the end of a request. Applications should still understand their actual connection lifecycle rather than assuming every request always receives a completely new database session.
Performance, storage, and memory tradeoffs
Isolation normally does not change the formal Big O complexity of the business algorithm. An indexed lookup remains an indexed lookup. However, it can significantly change operational cost.
Lock-based implementations may make transactions wait and can create deadlocks when transactions acquire incompatible locks in different orders. Multi-version implementations may retain older row versions while transactions or snapshots remain active. Those versions are normally stored and managed by the database, not in PHP application memory, but they can increase database storage, cleanup, vacuum, undo-log, or version-chain work depending on the engine.
Serializable implementations may block transactions or abort transactions that would complete at a weaker level. Retries consume additional database work, PHP execution time, and connection capacity.
Long transactions make these costs worse. Good indexes reduce unnecessary scanning and may narrow the rows or key ranges involved, but they do not guarantee that blocking, phantom protection, deadlocks, or serialization failures will disappear.