How does EK9 prevent partial transaction commits?
← Dependency Injection · Ref: Q338
EK9 makes partial commits much harder to cause through three structural mechanisms: try-with-resources guarantees that operator close runs, there is no early return, and the isCommitted() check lets close observe the transaction state. Rolling back is still something your code does explicitly.
MECHANISM 1: OPERATOR CLOSE ALWAYS RUNS
Transaction extends Closeable. When used with try-with-resources, operator close is called automatically when the scope exits, normally or through an exception. Close is a pure operator (E07500 if it is not declared 'as pure'), so it can read isCommitted() and report or clean up pure state, but it cannot call the non-pure rollback() (E08130). A class that implements Transaction without writing its own close inherits one that does nothing, and the compiler does not complain.
MECHANISM 2: NO EARLY RETURN
EK9 has no return statement. You cannot return early from the middle of a transaction, which is one of the most common causes of partial commits in other languages. The transaction scope always runs to completion or throws.
MECHANISM 3: isCommitted() SAFETY CHECK
Your implementation decides what isCommitted() returns. In the example it is false until commit() has been called. Operator close can check it and report an uncommitted transaction.
ROLLBACK IS EXPLICIT
Close cannot call rollback(), so do it where you can: in the catch block, which still sees the transaction.
try -> txn <- SafeTransaction("TXN-B") work() txn.commit() catch -> ex as Exception txn.rollback()
COMMON PARTIAL COMMIT SCENARIOS
1. EXCEPTION BETWEEN OPERATIONS: close still runs, and the catch block can call rollback()
2. EARLY RETURN: impossible in EK9 (no return statement)
3. FORGOT TO COMMIT: close sees isCommitted() is false and reports it
4. FORGOT TO ROLLBACK: rollback() has to be called explicitly (or the resource must itself discard uncommitted work when it is closed)
SPRING SILENT COMMIT BUG
In Spring, checked exceptions silently commit the transaction. A method that throws IOException commits partial data. In EK9 close runs whatever the exception type, so an uncommitted transaction is always seen; what you do about it is explicit.
See Q333 for try-with-resources pattern. See Q144 for no break/continue/return. See Q134 for exception handling. See Q137 for try-with-resources basics.
Example
defines module qa.di.transaction.no.partial defines class SafeTransaction with trait of Transaction identifier <- String() committed <- false operations <- 0 SafeTransaction() -> identifier as String this.identifier: identifier recordOperation() -> description as String <- status as String: `${identifier} op#${operations}: ${description}` operations: operations + 1 override commit() committed: true override rollback() committed: false operations: 0 override isCommitted() as pure <- rtn as Boolean: committed <?- Safety net: close always runs, so an uncommitted transaction is always reported. -?> override operator close as pure stdout <- Stdout() if committed stdout.println(`${identifier}: close after commit (${operations} ops)`) else stdout.println(`${identifier}: UNCOMMITTED on close (${operations} uncommitted ops)`) override operator ? as pure <- rtn <- true defines program NoPartialCommitDemo() stdout <- Stdout() // === SUCCESSFUL: all operations committed === stdout.println("=== Committed transaction ===") try -> txn <- SafeTransaction("TXN-A") status1 <- txn.recordOperation("debit account") stdout.println(status1) status2 <- txn.recordOperation("credit account") stdout.println(status2) txn.commit() catch -> ex as Exception stdout.println("Error: " + $ex) // === UNCOMMITTED: operator close always runs and reports it === stdout.println("=== Uncommitted transaction ===") try -> txn <- SafeTransaction("TXN-B") status3 <- txn.recordOperation("update inventory") stdout.println(status3) status4 <- txn.recordOperation("send notification") stdout.println(status4) // No commit — operator close reports it catch -> ex as Exception stdout.println("Error: " + $ex) stdout.println("Close always runs, so an uncommitted transaction is always reported")
Common mistakes
E08180 — Class fields must be initialized at declaration. Without initialization or the injection suffix '!', the compiler reports a field-not-initialized error. See ek9 -h E08180 for details.
Incorrect:
identifier as String
Correct:
identifier <- String()
Other ways to ask this
- How do I ensure all-or-nothing transactions in EK9?
- What prevents half-committed data in EK9?
- How does EK9 handle transaction atomicity?
Coming from another language?
Java Spring: checked exceptions silently commit, @Transactional(rollbackFor) needed for explicit control. Go: must remember defer tx.Rollback() after every tx.Begin(). Rust: Drop trait ensures cleanup but requires explicit commit. C#: TransactionScope.Complete() must be called explicitly. Python: context manager __exit__ handles cleanup. EK9: operator close always runs and can observe isCommitted(), no return prevents mid-transaction exit, rollback() is called explicitly (close is pure and cannot call it).
Keywords: commit, cleanup, dependency, migrate, prevent, rollback, return, atomicity, safety, structural, close, inject, partial