دفتر سليمان

Note

Unconfirmed Is a Real State

In digital systems, an action does not always end as “success” or “failure.” Sometimes the operation may have completed, but confirmation was lost. Treating that uncertainty as a real state leads to safer retries, clearer interfaces, and more trustworthy systems.

Most interfaces prefer certainty.

Saved.

Failed.

Sent.

Not sent.

Published.

Not published.

Success.

Error.

These states are convenient because they are easy to display and easy to understand.

Reality is less cooperative.

Sometimes a user presses Save, the request reaches the server, the database commits the change, and the confirmation never returns.

The browser sees silence.

The user sees uncertainty.

The user sees uncertainty.

The operation may have succeeded.

It may have failed.

The client simply does not know.

This is not the same as failure.

It is not success either.

It is unconfirmed.

And treating that as a real state is one of the small design decisions that can make a system dramatically safer.

Binary Thinking Breaks at the Network Boundary

Inside a single process, an operation can often be understood clearly.

A function returns success.

Or it throws an error.

Once a network appears between two systems, certainty becomes more complicated.

Consider a simple save operation.

The browser sends a request.

The server receives it.

The server validates the data.

The database commits the change.

The server sends a response.

The browser receives the response.

We usually think of that as one action.

It is actually a chain.

A failure can happen at any point.

If the request never reaches the server, the save failed.

If validation rejects the request, the save failed.

If the database transaction rolls back, the save failed.

But what if the transaction commits successfully and the connection disappears before the response reaches the browser?

From the server’s perspective, the save succeeded.

From the browser’s perspective, the outcome is unknown.

That distinction matters.

“Error” Can Be a Lie

Interfaces often display a generic message when a request times out:

That message may be wrong.

The system knows only that confirmation failed.

It does not necessarily know that the operation failed.

This seems like a small semantic detail.

It is not.

Imagine the user presses Save again.

If the operation is harmless to repeat, nothing serious happens.

But some actions are not harmless.

Creating an order.

Submitting a payment.

Publishing content.

Sending a message.

Creating a reservation.

Applying a database migration.

Triggering a workflow.

If the first operation actually succeeded, retrying blindly may create a duplicate.

The interface turned uncertainty into failure.

The user acted on that false certainty.

Now the system has a second problem.

Unknown Outcomes Are Normal in Distributed Systems

The deeper lesson is that uncertainty is not an exceptional condition created by poor programming.

It is a normal property of distributed systems.

Whenever one component asks another component to do something, the requester may lose contact before learning the result.

The network can disappear.

The process can restart.

The client can close.

A proxy can time out.

A response can be delayed.

A connection can be reset.

The receiving system may finish the work perfectly.

Communication about the result can still fail.

Once we accept this, system design changes.

Instead of asking only:

we also ask:

Those are different questions.

Acknowledgement Is Part of the Operation

Many workflows treat confirmation as presentation.

The real work is saving the data.

The success message is just UI.

I think that view is incomplete.

For important operations, acknowledgement is part of the workflow.

A useful save process does not merely change the authoritative state.

It gives the user enough evidence to know that the state changed.

That evidence might be:

a revision number,

a transaction ID,

a timestamp,

a new version,

a status transition,

or a visible representation of the saved data.

Compare two messages.

and:

The second message contains evidence.

If something becomes uncertain later, the user and the system have a shared reference.

The acknowledgement is not decoration.

It helps close the transaction cognitively.

Revision Numbers Are Surprisingly Powerful

Revision numbers are one of the simplest tools for reducing uncertainty.

Imagine an editor currently displays:

The user changes the title and saves.

The server responds:

Now both sides know something concrete.

If the connection breaks during a later save, the client can ask:

If the server reports 43, the uncertain operation probably succeeded.

If it still reports 42, the operation probably did not.

The system can verify instead of guessing.

This is much stronger than simply showing:

A revision turns vague state into something observable.

Verification Is Better Than Blind Retry

When an operation becomes uncertain, many systems immediately retry.

Sometimes that is appropriate.

Sometimes verification should come first.

Suppose a request times out.

The client could ask:

Or:

Or:

Or:

f the system can answer those questions, it may be able to resolve uncertainty without repeating the operation.

This creates a useful pattern:

Attempt → uncertainty → verify → retry only if necessary

That is safer than:

Attempt → timeout → retry

The difference becomes increasingly important as actions become more consequential.

Idempotency Makes Retry Safer

Sometimes verification is difficult or expensive.

In those cases, making operations safe to repeat can be extremely valuable.

This is where idempotency enters.

An idempotent operation produces the intended result once even if the same logical request is submitted multiple times.

Imagine the client creates a unique request identifier:

save-request-9f8a2

It sends the save.

The server commits it.

The response disappears.

The client retries with the same identifier.

The server recognizes:

It does not perform the operation twice.

Instead, it returns the existing result.

Now uncertainty becomes much less dangerous.

Retry is no longer equivalent to duplication.

The Identifier Must Represent Intent

A subtle detail matters here.

The idempotency key should represent the logical operation, not merely the HTTP request.

If every retry generates a new identifier, the server cannot know that the attempts represent the same intent.

For example:

First request:

publish-abc123

Retry:

publish-def456

The server sees two different publication requests.

Now compare:

First request:

publish-article-817-revision-24

Retry:

publish-article-817-revision-24

The system has a chance to recognize that both requests are trying to perform the same logical action.

Identifiers become part of the contract between intent and execution.

User Interfaces Need More Than Saving and Saved

Many interfaces expose only two states:

Saving…

Saved.

That works when everything goes well.

A more realistic model may include:

Clean.

Unsaved changes.

Validation error.

Saving.

Saved.

Save failed.

Save unconfirmed.

Conflict.

Unauthorized.

Archived.

Missing.

These states may seem elaborate.

They are not necessarily all visible at once.

The important point is that the system distinguishes them internally.

A user-facing message for an unconfirmed save might say:

That is a much more accurate description than:

And if verification succeeds:

The uncertainty was temporary.

The interface resolved it.

“Please Try Again” Is Sometimes Bad Advice

“Please try again” appears everywhere in software.

It is often reasonable.

It can also be dangerous.

Retrying a search request is harmless.

Retrying an operation that creates a financial transaction may not be.

Retrying a database migration certainly may not be.

Retrying a publication workflow may create duplicate downstream actions.

A mature system knows which operations are safe to repeat.

When they are not, it should provide a verification path.

The message presented to the user should reflect that knowledge.

Instead of:

perhaps:

The wording changes behavior.

Behavior changes risk.

Duplicate Prevention Is Not Only a UI Feature

A common solution is disabling the Save button after the first click.

Useful.

But that protects only one narrow path.

The user may refresh.

The application may retry automatically.

A mobile client may resend.

A reverse proxy may replay.

Another device may perform the same action.

Real duplicate prevention belongs deeper in the system.

The interface should help.

The server should still be authoritative.

This is a recurring architectural principle:

Prevent problems as close as possible to the layer that can guarantee the rule.

The UI can reduce accidental repeats.

The server should protect the invariant.

Conflicts Are Another Kind of Uncertainty

Consider another case.

A user begins editing revision 41.

While they are working, someone else saves revision 42.

The first user now presses Save.

Should their version overwrite the newer one?

If the system simply performs the update, information may disappear silently.

A safer model says:

That is a conflict.

Again, the system preserves meaning.

It does not collapse:

“save rejected because someone changed the document”

into:

“save failed.”

Those states require different responses.

A network failure might suggest verification.

A validation failure requires correcting input.

A conflict requires reconciliation.

The more accurately the system represents the state, the more intelligently the user can act.

Optimistic Concurrency Makes State Visible

One practical way to detect conflicts is optimistic concurrency.

The client says, in effect:

If the server is now on revision 42, it refuses to overwrite blindly.

The conflict becomes explicit.

This may use:

revision numbers,

timestamps,

ETags,

row versions,

or other concurrency tokens.

The implementation varies.

The principle is simple.

The save operation should know which state the user believed they were editing.

Without that knowledge, the system cannot reliably distinguish an update from an accidental overwrite.

Uncertainty Should Not Destroy the Draft

There is another basic rule worth stating.

When a save is uncertain, preserve the user’s work.

Do not clear the form.

Do not reset the editor.

Do not navigate away unless the operation can be reconstructed safely.

The authoritative system may or may not have the new version.

The client still has valuable state.

Preserve it until certainty returns.

This matters especially under unstable connectivity.

A failed confirmation should not turn a network problem into data loss.

The client may retain:

the unsent payload,

the edit revision,

the operation identifier,

and the time of the attempt.

Those pieces make recovery possible.

Local Drafts and Authoritative State Are Different

This connects to an earlier article in this series about surviving interruption.

A local draft can preserve progress.

But a local draft is not automatically the authoritative record.

That distinction should remain visible.

For example:

Or:

Or:

This is better than pretending synchronization already happened.

Good resilience does not erase the boundary between local and authoritative state.

It preserves both until they can be reconciled.

Logs Should Record Intent and Outcome

When an uncertain operation reaches support or debugging, logs become crucial.

Weak logging might show:

That tells us what the client experienced.

It does not tell us what happened.

Better logging connects the request to its outcome.

For example:

Request ID: 9f8a2

Received at: 10:31:04

Previous revision: 41

Result: committed

New revision: 42

Response sent: yes

Client acknowledgement: unknown

Now the operation can be reconstructed.

This is another form of a decision trail, but for execution.

The system keeps enough evidence to answer:

Observability Resolves Ambiguity

Reliable systems expose state.

Not necessarily to every user.

But somewhere.

Logs.

Metrics.

Revision history.

Transaction records.

Audit tables.

Status endpoints.

Operational dashboards.

Without observability, uncertainty becomes permanent.

With observability, uncertainty can often be reduced to a temporary condition.

This matters because uncertainty itself is not always dangerous.

Unresolvable uncertainty is dangerous.

A system can survive:

It struggles with:

That distinction is enormous.

Background Jobs Have the Same Problem

The pattern extends beyond Save buttons.

Suppose a user asks the system to generate a report.

The request creates a background job.

The page closes.

Later, was the report generated?

A robust system gives the job an identity and state:

Queued.

Running.

Succeeded.

Failed.

Canceled.

Perhaps unconfirmed is unnecessary because the authoritative job system can always be queried.

That is the ideal.

The user does not need to trust the original browser request forever.

They can ask the system:

Long-running operations become much easier to reason about when they have durable identity.

Automation Needs Confirmation Too

Automations are often treated as invisible workers.

A scheduled process runs.

Perhaps it succeeds.

Perhaps it fails.

If no evidence remains, we are depending on assumption.

Important automation should leave a trace.

What did it attempt?

What input did it use?

What changed?

Was the change committed?

Can the operation be rerun safely?

This does not mean drowning the system in logs.

It means preserving enough state to distinguish:

not executed,

executed successfully,

executed unsuccessfully,

and outcome uncertain.

Again, semantics matter.

Human Processes Have Unconfirmed States Too

The idea is not limited to software.

You send an important message.

No response arrives.

Was it read?

Unknown.

You submit an application.

No confirmation appears.

Was it received?

Unknown.

You ask someone to perform a task.

They say they will.

Has it been done?

Unknown until verified.

Human workflows often suffer from the same mistake:

we interpret silence as failure or success depending on our assumptions.

A better process creates acknowledgement.

Received.

Assigned.

Completed.

Confirmed.

These states reduce coordination cost.

Good communication protocols and good software protocols are surprisingly similar.

The Cost of Pretending

Why do systems collapse unknown into failure?

Because binary states are easier.

Database fields are simpler.

UI logic is simpler.

Testing is simpler.

Documentation is simpler.

But the complexity does not disappear.

It moves.

It appears later as:

duplicate requests,

lost work,

confused users,

manual reconciliation,

support incidents,

and unexplained inconsistencies.

The system saved complexity in the model and paid for it in operations.

This happens frequently in engineering.

We simplify representation by deleting a real state.

Reality eventually sends us the invoice.

Model the World You Actually Have

Several articles in this series have arrived at the same principle from different directions.

Zero is not the same as missing.

An observation is not the same as a decision.

A task is not the same as the reason behind it.

A local draft is not the same as authoritative state.

And a lack of confirmation is not the same as failure.

These distinctions may seem fussy.

They are the vocabulary that allows a system to describe reality accurately.

A model with fewer states is not automatically simpler.

If those states exist in the real process, ignoring them creates hidden complexity.

Good design does not aim for the smallest number of states.

It aims for the smallest number that still preserves the distinctions that matter.

A Practical Save Contract

For important editing workflows, I like a save contract with a few clear properties.

The client knows which revision it started from.

The save attempt has a unique identity.

The server validates that the expected revision is still current.

The server commits the change atomically.

A successful commit creates a new revision.

The response returns that revision.

If confirmation is lost, the client can query the authoritative state.

Retrying the same logical operation cannot create duplicate effects.

If another change occurred, the system reports a conflict rather than overwriting silently.

This sounds more complicated than:

It is.

It is also much more trustworthy.

And the user does not need to understand any of the machinery.

They simply experience a system that behaves predictably when conditions are imperfect.

Reliability Is Often About Preserving Meaning

We sometimes imagine reliability as stronger servers, better networks, or fewer crashes.

Those things matter.

But reliability is also semantic.

What state is the operation in?

What do we know?

What do we not know?

What can safely happen next?

A system that answers those questions clearly can recover from many technical failures.

A system that cannot answer them may remain fragile even on excellent infrastructure.

Yesterday’s article argued for improvements that can survive.

One way to build them is to preserve uncertainty instead of hiding it.

Do not force every interrupted operation into a convenient binary.

Sometimes the most accurate answer is:

We do not know yet.

Then design the system so that “yet” matters.

Give the operation an identity.

Preserve the user’s work.

Verify authoritative state.

Make retries safe.

Return evidence when certainty is restored.

Because unconfirmed is not failure.

It is not success.

It is a real state between them.

And systems become safer the moment they learn how to say so.

Filed under

  • Small Improvements That Can Survive
  • Systems Thinking
  • Resilient Workflows
  • Software Reliability
  • UX Design
  • Distributed Systems
  • Data Integrity