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