Failure Modeling
After the happy path works: what if payment fails, the request repeats, the database is down, the browser closes. Failure injected on purpose.
The basic path works. Now ask, step by step, what happens if the payment fails, the database is down, the request repeats, or the user closes the browser — and turn each answer into a state the system can be in and a test that puts it there.
"Payment failed" is not one event. The card can be declined, the provider can time out, the browser can close after the charge, the confirmation can arrive twice. The question that sorts them is: who says the payment succeeded, and how does my system find out?
A payment must not happen twice, and the double click, the browser retry, the webhook redelivery and your own retry loop all make the same request twice. Idempotency is the name for the answer; the lesson is discovering the question.
The payment succeeded and the backend crashed before the order was written. Two things that were supposed to be one thing happened separately, and the system is now in a state nobody designed. The move is to find every such gap and decide what fills it.
Once the store works, make it fail on purpose: a payment that times out, a product that runs out during checkout, a checkout submitted twice, a database that is not there. Each injection turns a failure you reasoned about into one you observed.
Five questions to ask of any design, at any boundary: what if it fails, what if it is slow, what if it repeats, what if it arrives out of order, what if it is unavailable. They are dull, they are the same every time, and that is why they work.