Transactions and Locking in SAP Commerce: Keeping Them Short, Correct, and Deadlock-Free
The transaction discipline behind a scalable checkout: short transactions, no remote calls inside them, optimistic-locking retries, and deadlock avoidance.
Dr. Elena Kovács
SAP Commerce Platform Architect
Core platform architecture, Spring, extension design, performance tuning, clustering, and JDK upgrades.
Transactions are where a commerce platform's correctness and its scalability meet, usually as a conflict. The checkout that must atomically reserve stock, create the order, and charge payment wants a transaction; the thousand concurrent checkouts want that transaction to hold no locks for longer than a heartbeat. Get the balance wrong and the symptoms are the worst kind: intermittent, load-dependent, and invisible in a single-user test. This guide covers transaction discipline in SAP Commerce, the locking behaviors that bite under concurrency, and the interaction with the cache model that makes "just wrap it in a transaction" more subtle than it looks. It builds on the persistence and caching guide (which established the cache coherence model) and the service layer guide (which is where transactions are usually scoped).
Scope Transactions Deliberately, Keep Them Short#
The platform provides transaction management, and the right tool is TransactionTemplate (or the transaction body pattern) rather than hand-managed begin/commit/rollback, because manual boundaries leak: an exception path that forgets to roll back, a commit that runs on a half-failed operation. Wrap the atomic unit and let the template handle the boundaries:
final Transaction tx = Transaction.current();
tx.execute(new TransactionBody() {
@Override
public Object execute() {
stockService.reserve(entry, quantity); // model writes
modelService.save(order);
return null;
}
});
The governing rule, stated as strongly as it deserves: a transaction should be as short as it can possibly be. Everything inside it holds locks and consumes a database connection; every millisecond inside it is a millisecond of contention under concurrency. Do the reads and the preparation outside the transaction, enter it only for the atomic writes, and exit immediately. A transaction that wraps a whole request handler, or that does its reads and computation inside the boundary, is a scalability ceiling waiting for peak traffic to find it.
The Cardinal Sin: Remote Calls Inside a Transaction#
The single most damaging transaction mistake in commerce, and the one the persistence guide flagged: never make a remote call inside a transaction. The classic instance is checkout calling a tax or payment service while holding the order's row locks:
- The remote call's latency (tens to hundreds of milliseconds, or a timeout of seconds) becomes lock-hold time.
- Every other transaction needing those rows waits for a network round-trip it has no control over.
- A slow or hung external service converts into cascading lock waits across your entire checkout flow, and a partner's bad afternoon becomes your outage.
The pattern is unambiguous: do the remote call outside the transaction, then open a short transaction to persist the result. Reserve stock and read what you need, call the tax service (no transaction), then enter a brief transaction to write the order with the tax result. If the external call and the persistence genuinely must be atomic (they rarely must), that is a saga or compensation problem (the events and webhooks guides), not a long transaction. Long transactions across network boundaries are how a healthy application tier produces a database-contention incident.
Optimistic Locking and Concurrent Updates#
SAP Commerce uses optimistic locking: concurrent modifications to the same item are detected at save time (via the item's modification tracking), and a losing writer gets a ModelSavingException/concurrent-modification failure rather than silently overwriting. The implications:
- Concurrent updates to the same item will occasionally fail, and that is correct behavior, not a bug. Two requests editing the same cart, two jobs touching the same product, the classic add-to-cart race: one wins, one must retry.
- Handle the conflict with a retry, not a crash. Re-read the current state, reapply the change, save again, bounded by a retry limit. The naive "load once, mutate, save" that ignores the possibility of a concurrent change is the code that throws intermittent errors under load and works perfectly in every single-user test.
- Design hot items to reduce contention: a single shared counter item that every checkout updates is a contention magnet; sharding it, or moving high-frequency updates out of the item model (to a queue processed serially, or an external store), removes the hotspot the persistence guide's mass-write warning also points at.
Deadlocks: Consistent Ordering#
Deadlocks arise when two transactions acquire the same locks in opposite orders: transaction A locks item X then wants Y, transaction B locks Y then wants X, and both wait forever until the database kills one. The defenses:
- Acquire locks in a consistent order. If your code touches multiple items in one transaction, always touch them in the same order (by PK, by type, by any total order), so two concurrent transactions cannot form the cycle. This is the single most effective deadlock preventer and it costs only discipline.
- Keep transactions small (again): fewer locks held for less time means fewer opportunities to form a cycle.
- Expect and retry deadlock victims. The database will kill one transaction to break a deadlock; that transaction should retry, the same as an optimistic-lock loser. Deadlock retry and optimistic-lock retry are the same defensive pattern.
- Watch for index and foreign-key locks. Deadlocks are not always on the rows you think; the database's index and constraint locking can create cycles your model-level reasoning misses, which is why the JDBC logging and database expertise from the hAC and on-prem guides matter when deadlocks appear in production.
Transactions and the Cache#
The interaction the persistence guide previewed, stated precisely: within a transaction, your reads see your own uncommitted writes, but other nodes see the pre-transaction state until you commit and the invalidation broadcasts. Consequences:
- Do not expect cross-node read-your-writes mid-transaction. Code that writes an item in one node's transaction and expects another node (or another request) to see it before commit is wrong by the cache model's design.
- Commit is when the world changes, followed by cache invalidation. The window between commit and full cluster invalidation is small but nonzero, so logic that depends on instantaneous cluster-wide consistency after a write is racing the invalidation broadcast.
- Large transactions are large invalidations. A transaction writing many items invalidates many cache entries across the cluster on commit (the persistence guide's mass-write cost), another reason to keep the write set focused.
Checklist#
- Transactions scoped with
TransactionTemplate/transaction body, not manual begin/commit - Transactions as short as possible: reads and computation outside, only atomic writes inside
- Zero remote calls inside any transaction; external call then short persist, never call-while-locked
- Concurrent-update code handles optimistic-lock failures with bounded re-read-and-retry
- Hot shared items sharded or moved off the model to reduce contention
- Multi-item transactions acquire locks in a consistent order to prevent deadlocks
- Deadlock and lock-timeout victims retried, not crashed
- No reliance on cross-node read-your-writes mid-transaction; write sets kept focused to bound invalidation
Transactions reward one instinct above all: keep them small, and keep the network out. A commerce platform under real concurrency punishes long transactions and lock-while-remote-calling ruthlessly and intermittently, which is the worst way to learn the lesson. Scope tightly, call externally outside the boundary, order your locks, retry your conflicts, and the checkout that works for one user keeps working for the ten thousand who arrive at once.