The Question Behind This

A database transaction wrapping multiple operations guarantees the data ends up consistent — everything commits together, or everything rolls back together. It guarantees nothing, however, about what the screen tells the person using the application while that transaction is still in progress.

This distinction surfaced through a reader's question on an earlier article about the Unit of Work pattern, and it's worth exploring on its own, since it's a genuinely separate concern from database-level correctness.

On a salary batch processing screen built around a shared transaction across two save steps, a reader asked whether the interface shows a single save failure when the second write fails, or whether it can make the first step appear successful before the rollback occurs.

For anyone reconciling payroll afterward, that distinction carries real weight.

Two Layers That Are Easy to Conflate

The database-level guarantee, when built correctly, holds reliably:

var batchResult = InsertBatchRecord(
    batchModel,
    connection,
    transaction);

if (batchResult.Success)
{
    var updateResult = UpdateRelatedRecords(
        batchResult.GeneratedId,
        relatedModel,
        connection,
        transaction);

    if (updateResult.Success)
    {
        transaction.Commit();
    }
    else
    {
        transaction.Rollback();
    }
}

A failure in UpdateRelatedRecords triggers a rollback that undoes everything, including the earlier batch insert.

The underlying data ends up consistent regardless of where the failure occurred.

The complication appears once the code also updates the interface at intermediate points:

var batchResult = InsertBatchRecord(
    batchModel,
    connection,
    transaction);

if (batchResult.Success)
{
    DisplayMessage(
        "Step 1: Batch record saved successfully!");

    var updateResult = UpdateRelatedRecords(
        batchResult.GeneratedId,
        relatedModel,
        connection,
        transaction);

    if (!updateResult.Success)
    {
        transaction.Rollback();

        DisplayMessage(
            "Step 2 failed. Transaction rolled back.");
    }
    else
    {
        transaction.Commit();

        DisplayMessage(
            "Both steps completed successfully.");
    }
}

The user sees:

"Step 1: Batch record saved successfully!"

before the second step has even executed.

If that second step fails and the rollback runs, the database correctly ends up with no batch record. Yet the user already saw, and potentially acted on, a success message describing data that no longer exists.

Even with a follow-up message explaining the rollback, the initial message was genuinely displayed. Anyone relying on a screenshot, log entry, or a passing glance at that moment holds evidence of a "success" that the database has since undone.

Why This Matters for Reconciliation

The reader's framing points directly at reconciliation.

Reconciliation typically relies on whatever evidence exists after the fact:

  • Screenshots

  • Support tickets

  • Log entries

  • User statements

  • Database records

An intermediate success message that a subsequent rollback later invalidates can become misleading evidence for anyone reconstructing events afterward.

For example, a support ticket might state:

"The batch was saved. I saw the confirmation."

The database may later show that no batch record exists.

This does not necessarily indicate an error in the transaction logic. The transaction may have behaved correctly. The problem is that the interface communicated something that was only temporarily true from the application's perspective.

A Safer Pattern: One Final Status Only

The straightforward correction is to withhold success communication until the transaction has genuinely resolved:

var batchResult = InsertBatchRecord(
    batchModel,
    connection,
    transaction);

bool overallSuccess = false;

if (batchResult.Success)
{
    var updateResult = UpdateRelatedRecords(
        batchResult.GeneratedId,
        relatedModel,
        connection,
        transaction);

    if (updateResult.Success)
    {
        transaction.Commit();
        overallSuccess = true;
    }
    else
    {
        transaction.Rollback();
    }
}
else
{
    transaction.Rollback();
}

DisplayMessage(
    overallSuccess
        ? "Batch processed successfully."
        : "Batch processing failed. No changes were saved.");

No intermediate success messaging occurs.

A single final status appears only after the commit or rollback has genuinely taken place. This eliminates the possibility of a UI message describing a state that a later rollback could invalidate.

Progress Indicators Without False Claims

Some workflows genuinely benefit from showing progress during a longer multi-step operation.

In those cases, progress indicators should communicate ongoing activity rather than completion.

For example:

Processing step 1 of 2...

is different from:

Step 1 saved successfully.

The first statement makes no claim of permanence that a later rollback could contradict.

The second statement communicates a completed state that may subsequently be undone.

A useful distinction is:

Progress message

"Processing step 1 of 2..."

Final success message

"Batch processed successfully."

The final message should only be shown after the transaction has committed successfully.

A Review Question Worth Adding

When reviewing a multi-step save, ask two separate questions:

  1. Does the database remain consistent when a failure occurs?

  2. Does any user-facing message appear before the transaction actually resolves in a way that could later become misleading?

Satisfying the first question does not guarantee satisfying the second.

A transaction can be completely correct at the database level while sitting behind an interface that briefly communicates a different state.

The Difference Between Database State and UI State

The underlying issue can be represented simply:

Application Operation
        |
        ↓
   Transaction
        |
   ┌────┴────┐
   ↓         ↓
Write 1    Write 2
   |         |
   └────┬────┘
        ↓
   Commit/Rollback
        |
        ↓
   Final Database State

The UI, however, can communicate information at any point:

Application Operation
        |
        ├── UI message
        |
        ├── Write 1
        |
        ├── UI message
        |
        ├── Write 2
        |
        └── Commit/Rollback

That means UI communication and transaction state need to be treated as separate concerns.

The database transaction determines what ultimately persists. The UI determines what the user believes is happening.

Takeaway

Database-level transactional correctness and accurate interface communication are related concerns, but they are not the same thing.

A properly implemented shared transaction guarantees consistent data, but says nothing about what a user sees in the moments before that consistency is achieved.

The more reliable approach is to withhold success messaging until the operation has genuinely committed. Progress indicators can communicate that work is still underway, while final success or failure messages should reflect the transaction's actual outcome.

This ensures that whatever a user sees — and whatever ends up captured in a screenshot, log, or support ticket — reflects the database's actual final state rather than a provisional condition that a subsequent rollback might erase.