Handling Production Incident

The Production Incident

LugemineEelvaade

Vaatad tasuta eelvaate õppetundi.

Our Order Management Backend is now running as a production-style system.

We have already built:

Product management

Inventory management

Order creation and cancellation

Payment integration

authentication and authorization

PostgreSQL transactions

automated testing

structured logging

request correlation

health checks

metrics

container packaging

CI

Until now, most of our engineering work followed this direction:

requirement
    ↓
design
    ↓
implementation
    ↓
testing
    ↓
shipping

Now something changes.

Production sends information back to us.

A Customer reports:

I tried to place one Order, the request appeared to fail, so I tried again. Now I have two Orders.

This begins:

Module 13 — Investigating a Production Incident

Our first goal is not:

write a fix

Our first goal is:

Understand what actually happened.


The Incident Report

Support receives a Customer report.

The Customer says:

I submitted an Order.

The application kept loading.

Eventually I received an error.

I tried again.

The second attempt succeeded.

Later I noticed two Orders
for the same items.

Support checks the Customer's Order history.

They find two Orders.

Conceptually:

Order A

customerId = customer-742

status = UNPAID

items:
    Product P1 × 2
    Product P2 × 1

and shortly afterwards:

Order B

customerId = customer-742

status = UNPAID

items:
    Product P1 × 2
    Product P2 × 1

The IDs are different.

For example:

Order A
36a25516-2af2-4644-b55d-d0cabf389c9e
Order B
b29c51cc-f75a-46ae-a74c-b4c342547a20

Both Orders are structurally valid.

Both belong to the same Customer.

Both contain valid OrderItems.

Both consumed Inventory.

This is not merely:

duplicate UI rendering

There are actually two persisted Orders.


Initial Incident Statement

We can describe the known production symptom as:

A Customer retry after an uncertain Create Order response resulted in two distinct Orders for what the Customer considered one logical purchase attempt.

That statement is intentionally careful.

We are not yet saying:

frontend bug

or:

database bug

or:

network bug

or:

transaction bug

or:

idempotency bug

Those may become conclusions later.

At the beginning of an incident, distinguish:

observed facts

from:

hypotheses

Create an Incident, Not Just a Slack Thread

Earlier in the course we reserved:

docs/incidents/

for production incident reviews.

This incident should become part of the engineering record.

Conceptually:

docs/
└── incidents/
    └── duplicate-order-after-retry.md

We will build that document as the investigation progresses.

Do not write the final root cause before investigating it.


Create the Corrective Backlog Item

The original course plan already anticipated a bug such as:

BUG-201
Prevent duplicate order creation
under identified retry scenario

Notice the wording.

It does not say:

Add an idempotency table.

It does not say:

Add Redis lock.

It does not say:

Add UNIQUE(customer_id, product_id).

It states the behavior we need to protect.

The investigation will determine the appropriate design.


The First Rule During an Incident

Do not start coding immediately.

A production incident creates pressure.

Someone may say:

Just add a retry check.

Just compare recent Orders.

Just disable the button.

Just add a database constraint.

Just make the POST retry-safe somehow.

Those may sound useful.

But acting before understanding the failure can:

hide evidence

introduce another bug

solve only one symptom

break legitimate behavior

The first engineering task is:

establish facts

What Do We Know?

From the Customer report:

one intended purchase

appears to have created:

two Orders

From PostgreSQL:

both Orders exist

From Inventory:

both Orders consumed stock

From the second request:

the Customer eventually received success

From the Customer:

the first request did not appear successful

That is enough to begin investigation.

It is not yet enough to claim exact root cause.


What Do We Need to Determine?

Important questions:

Did the first request reach the backend?

Did CreateOrderUseCase execute?

Did the first transaction commit?

Was a 201 response generated?

Did the client receive it?

Was the second request a retry?

Were the request payloads equivalent?

Did both requests have different request IDs?

Did both requests generate new OrderIds?

Was Inventory consumed twice?

Were there any database rollbacks?

Was there an application restart?

Was there an HTTP/network interruption?

Are other Customers affected?

These questions move us from:

duplicate Order happened

toward:

how duplicate Order happened

Start With Persistent Truth

Our earlier transaction design established an important principle:

Persisted committed state is the source of truth.

So begin with PostgreSQL.

Do not begin by assuming:

the client saw an error
therefore the first request failed

That conclusion is unsafe.


Client Failure Is Not Necessarily Backend Failure

Consider:

server successfully commits

and:

client reports timeout

These statements can both be true.

The HTTP connection and the database transaction are separate boundaries.

The client knows:

I did not receive a successful response.

It does not automatically know:

the backend transaction rolled back.

That distinction is central to this incident.


Revisit the Create Order Transaction

Our Create Order workflow is approximately:

validate request
    ↓
validate Products
    ↓
capture current priceCents
    ↓
consume Inventory
    ↓
construct Order
    ↓
persist Order + OrderItems
    ↓
COMMIT

All local database mutation is protected by the transaction.

The golden rule remains:

complete Order + Inventory mutation

or

no committed mutation

That protects database consistency.


What the Transaction Guarantees

Suppose the transaction commits.

Then:

Inventory consumption is committed

Order exists

OrderItems exist

Suppose it rolls back.

Then:

Inventory mutation disappears

Order does not exist

OrderItems do not exist

This prevents:

Inventory consumed
but no Order

or:

partial OrderItems

That is strong transactional correctness.


What the Transaction Does Not Guarantee

It does not guarantee:

the Customer received the HTTP response

Consider:

BEGIN

consume Inventory

persist Order

COMMIT

Then:

server tries to return 201

Between those events:

network connection may disappear

application process may terminate

proxy may time out

client may disconnect

response may be lost

PostgreSQL cannot roll back a transaction that already committed merely because the HTTP response later became uncertain.


The Dangerous Window

The failure window looks like:

Create Order request
        ↓
transaction executes
        ↓
COMMIT succeeds
        ↓
Order is now durable
        ↓
        X
response does not reach Customer

From the backend's database perspective:

SUCCESS

From the Customer's perspective:

UNKNOWN

This is a very different problem from:

transaction failed

The Customer Retries

The Customer sees:

timeout

or another uncertain outcome.

Retrying seems reasonable.

The Customer sends another:

POST /api/v1/orders

with the same intended items.

Our current Create Order contract contains:

Product IDs

quantities

but no request-level operation identity.

The backend therefore sees:

new HTTP request

and executes:

new Create Order operation

A New Request Generates a New OrderId

Our canonical ID strategy is:

application-generated UUID

Each Create Order execution generates a new:

OrderId

So:

Request 1
→ OrderId O1

and:

Request 2
→ OrderId O2

Nothing in the current contract tells the backend:

these two requests represent
the same logical Customer operation

Both Requests Can Be Individually Valid

This is the subtle part.

Request 1 may be valid.

Request 2 may also be valid.

For each execution independently:

Customer authenticated
Products exist
Products are active
Inventory is sufficient
quantities are valid
Order Domain invariants hold
database transaction succeeds

So the backend can create:

two perfectly valid Orders

while still violating the Customer's intended behavior.


Database Integrity Does Not Detect This

Our database protects:

valid UUID primary keys

valid foreign keys

valid Order status

positive quantities

non-negative prices

unique Product within one Order

But these two records:

Order O1
Customer C1
Product P1 × 2

and:

Order O2
Customer C1
Product P1 × 2

do not violate any relational constraint.

Customers are allowed to place the same Order twice legitimately.

Therefore we cannot simply declare:

same Customer
+
same Products
=
duplicate

That would be incorrect.


Legitimate Duplicate-Looking Orders Exist

Imagine a Customer intentionally buys:

2 keyboards

at 10:00.

Then five minutes later buys:

2 more keyboards

Those are two legitimate Orders.

Or:

same Products

same quantities

same Customer

could represent genuine repeated purchases.

So this constraint would be wrong:

UNIQUE(
    customer_id,
    items
)

Even if such a schema were easy to express, the business meaning would be incorrect.


Time-Based Deduplication Is Also Dangerous

Another tempting fix:

If same Customer submits
same items within 30 seconds,
reuse previous Order.

What happens when the Customer genuinely wants:

two separate purchases
within 20 seconds?

The backend would silently discard legitimate intent.

Time similarity is evidence.

It is not a reliable operation identity.


UI Button Disabling Does Not Solve It

The frontend could disable:

Place Order

after the first click.

That is useful UX.

It does not solve backend correctness.

Why?

Retries may originate from:

browser

mobile application

proxy

SDK

network retry logic

manual API call

And the incident does not require a double-click.

The first request may genuinely become ambiguous.

The backend still needs a safe semantic answer for retries.


Request ID Does Not Solve It

We introduced:

X-Request-Id

for observability.

Could we deduplicate on:

requestId

?

Not automatically.

Request ID means:

this HTTP execution

If a client retries:

Request A
requestId = R1
Request B
requestId = R2

those are correctly two request executions.

Operational correlation is not business idempotency.


OrderId Does Not Solve It Either

The server currently generates the OrderId.

The client does not know:

OrderId O1

until it receives the successful response.

If that response is lost, the client cannot necessarily retry with:

OrderId O1

because it never learned it.

So current server-generated Order identity does not identify the logical client operation.


Payment Idempotency Does Not Solve Create Order Idempotency

We already studied idempotency carefully around Payment.

But Payment has its own:

PaymentAttempt

coordination.

That does not automatically make:

POST /api/v1/orders

idempotent.

Different operations require different identities and semantics.

Do not assume:

we solved idempotency once
therefore all POST requests are safe to retry

HTTP POST Is Not Automatically Retry-Safe

Our Create Order endpoint:

POST /api/v1/orders

creates new state.

If executed twice:

execution 1
→ O1

execution 2
→ O2

That is normal POST behavior unless we deliberately define additional idempotency semantics.

The incident is exposing a requirement we did not previously implement.


Was the Original Design Wrong?

Be careful with hindsight.

Earlier we explicitly recognized:

request-level idempotency

as a potential production concern.

We chose not to invent an idempotency contract before a concrete requirement existed.

That was intentional scope control.

Now production has given us:

concrete failure

concrete impact

concrete requirement

The backlog should evolve.

This is how long-lived systems work.


Production Changes Requirements

Before the incident, requirement was effectively:

Customer can create an Order.

Now we have learned an additional behavioral requirement:

Retrying the same logical Create Order operation after an uncertain response must not create another Order.

That is new product/system behavior.

It deserves design.


But We Are Not Designing It Yet

This lesson is about the incident.

Do not jump directly to:

Idempotency-Key header

or:

new table

or:

Redis

We first need to prove the failure timeline from production evidence.

The next lessons will move through:

evidence
    ↓
timeline
    ↓
root cause
    ↓
corrective design

in that order.


Incident Investigation Uses What We Already Built

This is where Module 11 becomes practical.

We now have:

structured logs

request IDs

metrics

health checks

These were not decorative production features.

They provide evidence.

We can investigate:

request R1

request R2

Order O1

Order O2

through logs and persisted state.


Start With the Two Orders

Suppose we identify:

Order O1

createdAt = T1

and:

Order O2

createdAt = T2

where:

T2

is shortly after:

T1

Both belong to:

Customer C1

Both have:

Product P1 × 2

Product P2 × 1

This is important evidence.

It still does not prove retry behavior.


Compare Historical Prices

Recall:

OrderItem.unitPriceCents

captures purchase-time Product price.

If both Orders have the same:

unitPriceCents

that may strengthen the suspicion that they originated close together.

But again:

same price

does not prove duplication.

We collect evidence without overstating it.


Inspect Inventory Effects

If both Orders committed, Inventory should reflect:

consumption for O1

+

consumption for O2

Example:

Initial:

P1 = 10

P2 = 5

Order O1 consumes:

P1 × 2

P2 × 1

State becomes:

P1 = 8

P2 = 4

Order O2 consumes the same quantities.

State becomes:

P1 = 6

P2 = 3

If that is what production state shows, the second Order was not just duplicated presentation.

Two independent transactional workflows executed.


This Means Transactions May Have Worked Correctly

That feels counterintuitive.

We have an incident.

Yet the database transaction may have behaved perfectly.

For O1:

Inventory consumed
Order persisted
OrderItems persisted
COMMIT

For O2:

Inventory consumed
Order persisted
OrderItems persisted
COMMIT

Each transaction preserves our local invariant.

The incident exists at a different boundary:

logical request identity
across retries

Correctness Has Multiple Layers

This is a major backend engineering lesson.

A system can be:

transactionally correct

but:

behaviorally incorrect

for a higher-level workflow.

Similarly, a database can be:

relationally valid

while representing:

wrong business outcome

Correctness is not one property.


Search Logs by OrderId

Structured logs may contain:

event=order_created
orderId=O1
customerId=C1
requestId=R1

and:

event=order_created
orderId=O2
customerId=C1
requestId=R2

This tells us something critical:

two different request executions
created two different Orders

Now we can investigate each request separately.


Search Request R1

For the first request we might find a sequence such as:

requestId=R1
request received
requestId=R1
event=order_created
orderId=O1

Then perhaps:

connection terminated

or:

application shutdown

or simply no evidence that the Customer received the response.

The exact production cause must come from actual evidence.

Do not invent missing log lines.


Search Request R2

The second request may show:

requestId=R2
request received

then:

requestId=R2
event=order_created
orderId=O2

then normal completion.

If R2 occurs shortly after R1 and matches the Customer report:

retry hypothesis becomes stronger

What If the First Request Has No Success Log?

Do not immediately conclude:

O1 was not committed

Remember the timing of logs and transactions.

A success log could:

not exist

fail to flush

occur before/after another failure

be missing due to instrumentation gaps

The database is authoritative for committed Order state.

Logs help reconstruct execution.

They do not replace persistence.


What If the Client Says "500"?

Even then, investigate.

Possible:

Order committed

response serialization failed

client received 500

Earlier in the course we explicitly recognized:

HTTP response formatting

as a different boundary from:

database transaction outcome

A 500 response does not automatically imply:

Order rolled back

What If the Client Says "Timeout"?

Even more ambiguous.

Timeout means:

client stopped waiting

It does not reveal exactly where the backend was.

Possible:

request never reached server

or:

request reached server
but transaction failed

or:

transaction committed
but response was delayed/lost

The incident investigation must determine which occurred.


Do Not Blame the Network Yet

The Customer's description may suggest:

network issue

But the system still has a responsibility to handle ambiguous retry situations safely if retry semantics are required.

Even if a network interruption triggered the incident:

network interruption

may be a trigger.

It is not necessarily the root cause of:

duplicate durable Orders

Root cause analysis must distinguish these.


Trigger vs Root Cause

Example:

Trigger:
HTTP response was not received.

Possible underlying system weakness:

Create Order had no way
to recognize retry of
the same logical operation.

These are not the same statement.

A strong incident review separates:

trigger

contributing factors

root cause

impact

Do Not Blame the Customer

Another weak conclusion:

Customer clicked twice.

Even if true, ask:

Was the operation expected
to tolerate retry?

Did our UX encourage retry?

Could the first result
have been unknowable?

Could an automated client
do the same thing?

Reliable backend design should not rely only on:

humans never retry

Do Not Blame the Frontend

A frontend may eventually help by:

disabling repeated submission

showing pending state

persisting an operation key

But the backend owns:

durable business side effects

A robust backend API must define the semantics of repeated side-effecting requests.

UI protection can supplement that.

It cannot be the only correctness mechanism.


Do Not Add an Arbitrary Retry Framework

Another reaction might be:

Maybe the server should automatically retry Create Order.

That can make the problem worse.

Create Order:

creates a new OrderId

consumes Inventory

Blindly rerunning the workflow can produce exactly the duplication we are investigating.

Retry safety requires:

operation identity

clear semantics

not simply:

try again

Do Not Delete One Order During Investigation

Operations might be tempted to:

DELETE duplicate Order

immediately.

But our database intentionally protects historical state.

We do not have a general Order deletion capability.

Deleting production records can also destroy evidence.

Incident response should first establish:

which Order is valid

what downstream effects occurred

whether payment happened

whether Inventory needs correction

before deciding remediation.


Do Not Manually "Fix Inventory" First

If both Orders consumed Inventory, changing:

available_quantity

manually before understanding Order state can create new inconsistency.

For example:

restore Inventory for O2

while O2 later becomes a legitimate paid Order.

Operational remediation must follow business truth.

Investigation comes first.


Scope the Impact

One Customer report does not tell us whether this is:

one incident

or:

systematic production behavior

We need to ask:

How many duplicate-looking Orders exist?

Did this start recently?

Is it associated with timeouts?

Is it associated with one client type?

Is it associated with one application version?

Are any duplicate Orders PAID?

How much Inventory was consumed?

Did any Product become unavailable incorrectly?

This is incident impact analysis.


Do Not Detect Impact Using a Naive Equality Rule

A query such as:

same customer
+
same items
+
close timestamp

can find:

candidates

It cannot automatically label them:

duplicates

because legitimate repeated purchases can look identical.

Candidate analysis may require:

logs

request timeline

support evidence

request metadata

Metrics May Reveal Scale

Our custom metrics may tell us:

order creation rate

technical failure rate

Built-in HTTP metrics may show:

POST /api/v1/orders latency

5xx rate

These can help answer:

Did something unusual happen
around the incident time?

Metrics detect patterns.

They generally do not identify:

which exact Order

because we deliberately avoided high-cardinality Order IDs in metric tags.


Logs Identify Individual Executions

This is exactly why our observability design separated:

metrics

from:

logs

Metrics:

How many?
How often?
How long?

Logs:

What happened to R1?
What happened to O1?

Incident response combines them.


Health May Rule Out Certain Hypotheses

Health history may tell us whether:

PostgreSQL readiness

was unhealthy around the time of the incident.

If PostgreSQL remained healthy:

database outage becomes less likely

as the trigger.

If readiness dropped:

database instability deserves investigation

Health does not prove the duplicate mechanism.

It contributes context.


CI Cannot Prevent Every Production Incident

Our CI may be completely green.

Every existing test may pass.

Why?

Because we had not yet specified:

retry same logical Create Order
after uncertain response
→ return/reuse same outcome

A test suite protects:

known requirements

known risks

known regressions

Production sometimes discovers:

new requirement

or:

missing scenario

This does not make testing useless.

It tells us how the test suite should evolve.


The Incident Must Become a Regression Test

Once root cause and desired semantics are established:

incident scenario

should become:

automated regression test

Then future refactoring cannot silently reintroduce the same failure.

That happens later in this module.

For now, preserve the scenario precisely.


Initial Incident Timeline

At the beginning of investigation, we can record a provisional timeline.

For example:

T1
Customer submits Create Order request R1.

T2
Backend processes R1.

T3
Order O1 appears in committed database state.

T4
Customer does not receive a successful response.

T5
Customer retries as request R2.

T6
Backend processes R2.

T7
Order O2 commits.

T8
Customer receives successful response for R2.

T9
Customer notices O1 and O2.

This is a working timeline.

Every point should later be tied to evidence.


Label Unknown Parts

For example:

T3
O1 committed.
Evidence:
database state / createdAt / logs.
T4
Customer did not receive success.
Evidence:
Customer report.

But:

Why did R1 response fail?

may remain:

UNKNOWN

Do not fill gaps with confidence you do not have.


Incident Documents Should Distinguish Evidence

A useful structure:

Observed

Confirmed

Suspected

Unknown

For example:

Confirmed

Two Orders exist.

Both belong to the same Customer.

Both consumed Inventory.

Reported

Customer did not receive
the first successful response.

Suspected

Second request was a retry
of the same logical operation.

Unknown

Exact reason first response
did not reach the Customer.

This keeps the investigation honest.


Severity

We do not have an organizational severity framework defined in this course.

So do not invent:

SEV-1

P0

Critical-7

without a real company convention.

We can still describe impact:

Customer can receive duplicate Orders.

Inventory can be consumed twice.

If duplicate Orders are later paid,
financial impact may occur.

That is enough to establish seriousness.


Potential Business Impact

Duplicate Orders can affect more than one table.

Possible consequences:

Inventory reduced twice

Customer confusion

Customer may pay twice

support workload

incorrect Order history

downstream operational confusion

Do not claim all of these occurred.

They are:

potential impact

that investigation should assess.


Payment Makes the Incident More Serious

If both Orders remain:

UNPAID

we still have a correctness issue.

If both become:

PAID

potential Customer financial impact exists.

Therefore investigation should check:

status of O1

status of O2

PaymentAttempt state
for each Order

Do not assume the duplicate Order incident is isolated from Payment.


Cancellation Is Not Automatically the Remedy

Could we simply cancel one duplicate Order?

Maybe.

But determine:

which one

whether it is still UNPAID

whether a Payment attempt is unresolved

whether cancellation is permitted

whether Inventory restoration is safe

Recall our established rule:

unresolved Payment attempt
can block cancellation

Incident remediation must respect existing business invariants.


Production Remediation and Permanent Fix Are Different

We may eventually need two tracks.

Immediate remediation

For affected Customers:

inspect Orders

determine valid business state

correct safely

Permanent corrective work

prevent equivalent retry scenario
from creating duplicate Orders

Do not confuse:

repair existing data

with:

prevent future incidents

Do We Need an Emergency Code Change?

Not necessarily.

First determine:

frequency

impact

reproducibility

safe mitigation

If incidents are frequent and severe:

urgent mitigation

may be appropriate.

If evidence is incomplete:

investigation

may be safer than rapidly shipping the wrong fix.

Incident response is risk management.


Potential Temporary Mitigations

Depending on evidence, a real team might consider:

support guidance

client retry guidance

temporary feature restriction

increased monitoring

manual review

But our source does not define a specific operational environment or mitigation.

We will not invent one.

The course focuses on:

understanding

designing

implementing

testing

the corrective backend behavior.


Create the Incident Investigation Questions

Before the next lesson, our investigation checklist is:

1. Confirm both Orders exist.

2. Confirm both transactions committed.

3. Compare Customer and OrderItems.

4. Find request IDs related to both Orders.

5. Reconstruct request sequence.

6. Determine whether R2 was a retry.

7. Determine what happened to R1 response.

8. Check Inventory effects.

9. Check Payment state.

10. Assess whether other Customers
    experienced similar behavior.

11. Determine whether deployment,
    database health, or network behavior
    contributed.

12. Identify the system condition
    that allowed one logical operation
    to create two Orders.

Only then should we design the fix.


What We Should Not Change Yet

At this stage, do not add:

Idempotency-Key

yet.

Do not create:

idempotency_requests

table yet.

Do not add:

Redis

yet.

Do not add:

unique index

yet.

Do not change:

OrderId generation

yet.

Do not implement:

automatic retries

yet.

Do not change:

POST /api/v1/orders

contract yet.

Those decisions belong after root cause and requirements are explicit.


The Key Engineering Distinction

This incident teaches a distinction that is easy to miss:

database atomicity

is not:

request idempotency

Transaction gives us:

one execution
→ all local database changes
   commit together

Idempotency would give us something like:

same logical operation
executed repeatedly
→ one intended business effect

These are different guarantees.


Another Important Distinction

Likewise:

HTTP response

is not:

business transaction

The server can:

commit successfully

while:

client observes failure

Reliable backend design must reason across both boundaries.


Why This Incident Is Useful

Until now we designed failure handling mostly from known requirements.

This incident forces us to experience another part of backend engineering:

Production behavior can expose missing assumptions that were invisible during the first implementation.

A strong engineer must be able to:

understand existing system

collect evidence

avoid premature conclusions

identify the failed assumption

design a compatible change

protect existing data and clients

add regression coverage

That skill is more important than merely knowing a new annotation.


Backlog Evolution

Our original backlog ended at:

BACKEND-128

But production systems do not have a truly finished backlog.

Now reality adds:

BUG-201
Prevent duplicate order creation
under identified retry scenario

This is expected.

A backlog represents:

current understanding

not eternal truth.


Do Not Rewrite History

An incident review should not say:

We should obviously have known this.

The earlier design explicitly recognized the possibility but intentionally did not invent an idempotency contract without a requirement.

A useful review asks:

What did we know?

What assumption did we make?

What did production teach us?

What should change now?

This produces learning rather than blame.


Blameless Does Not Mean Responsibility-Free

Avoid:

nobody did anything wrong
so nothing needs to change

We still need to identify:

system weakness

missing requirement

missing test

missing operational signal

process improvement

Blameless investigation means:

focus on system conditions
rather than personal blame

Corrective action remains essential.


Initial Incident Record

At the end of this lesson, our incident record can begin like this:

Incident:
Duplicate Order Created After Retry

Status:
Under Investigation

Impact:
At least one Customer observed two persisted Orders
after retrying an Order creation request whose first
response appeared unsuccessful.

Known:
- Both Orders exist in PostgreSQL.
- Both Orders have distinct UUIDs.
- Both belong to the same Customer.
- Both contain equivalent requested items.
- Both committed Inventory effects.
- Customer reports retrying after an uncertain first response.

Unknown:
- Exact reason the first response was not received.
- Whether similar incidents affected other Customers.
- Whether both Orders entered Payment processing.
- Exact system timeline across both requests.

Suspected Risk:
Create Order currently has no request-level identity
that allows the backend to recognize a retry of the
same logical operation.

Next Action:
Reconstruct the incident using persisted state,
structured logs, request IDs, metrics, and runtime evidence.

Notice:

Suspected Risk

is not yet labeled:

Root Cause

That comes after investigation.


Engineering Principle

The first principle:

During an incident, establish what happened before deciding how to fix it. Production symptoms, hypotheses, and confirmed facts are not the same thing.

The second:

A successful database transaction and a failed client experience can occur simultaneously. Transaction atomicity protects database consistency; it does not tell the client whether its response was delivered.

The third:

Repeated side-effecting requests require explicit semantics if the system must treat them as one logical operation. Similar payloads, timestamps, request IDs, and UI protections are not substitutes for a deliberate operation identity.

And:

Production incidents are inputs to system design. The backlog, tests, documentation, and architecture should evolve as the system teaches us something new.


Summary

In this lesson, we established that:

  • Module 13 begins with a production incident rather than a new greenfield feature.
  • A Customer reports two Orders after retrying an uncertain Create Order request.
  • Two distinct Orders exist in PostgreSQL.
  • Both Orders may be individually valid.
  • Both may have consumed Inventory correctly.
  • An incident does not automatically imply transaction corruption.
  • Production investigation starts with facts rather than proposed fixes.
  • Persistent committed state is the primary source of truth for database outcomes.
  • Client-visible failure does not prove the server transaction failed.
  • A transaction may commit before the HTTP response is lost.
  • PostgreSQL atomicity protects one Create Order execution.
  • PostgreSQL does not know whether two requests represent one logical Customer intention.
  • The current Create Order contract generates a new OrderId for each execution.
  • Equivalent Order payloads do not reliably identify a retry.
  • Legitimate Customers may intentionally place identical Orders.
  • Time-window deduplication can incorrectly suppress legitimate purchases.
  • UI button disabling does not provide backend retry safety.
  • X-Request-Id is operational correlation, not idempotency identity.
  • OrderId does not solve the retry problem because the server generates it and a client may never receive it.
  • Payment idempotency semantics do not automatically make Create Order idempotent.
  • POST creation is not automatically retry-safe.
  • The original design intentionally did not invent request-level Create Order idempotency before a concrete requirement existed.
  • Production has now provided such a concrete scenario.
  • Structured logs, request IDs, metrics, health history, and PostgreSQL state provide investigation evidence.
  • Candidate duplicate Orders must not automatically be classified using only Customer/items/time similarity.
  • Metrics identify patterns while logs identify individual executions.
  • CI can be green while a previously unspecified production scenario remains unprotected.
  • Once the incident is understood, it must become regression coverage.
  • An incident timeline should distinguish confirmed facts, reports, hypotheses, and unknowns.
  • We should not invent an organizational severity scheme that the course has not defined.
  • Potential impact includes Inventory, Customer experience, and possibly Payment, but actual impact must be verified.
  • Existing Payment and cancellation invariants still apply during remediation.
  • Repairing existing production state and preventing future recurrence are separate tasks.
  • We should not add an idempotency mechanism before completing the investigation.
  • BUG-201 now represents the corrective backlog work created by production reality.

Next lesson:

Investigating With Production Signals

We will take the incident from:

Customer says:
"I got two Orders."

to an evidence-backed timeline using:

PostgreSQL state

Order createdAt values

structured logs

requestId

orderId

customerId

PaymentAttempt state

HTTP/application metrics

health history

and learn how to distinguish:

evidence

correlation

inference

root cause

without jumping directly to a fix.