A single database transaction is isolated: while it runs, nobody else sees its half-done changes, and two transactions touching the same row take turns. Sagas give that up. Each step commits immediately, so between step 1 and step 5, the whole world can see and act on a half-finished saga.
That's fine when sagas don't touch the same data. At Tiffin's scale, they often do. Two checkouts by the same customer, a cancel arriving during a checkout, the last portion of the chef's special. This lesson covers what goes wrong when sagas overlap, and the handful of practical techniques that keep it under control.
Three things that go wrong
Database people have names for these. You'll recognise them from how isolation levels are described, but here they happen between services, where no database can prevent them.
Sagas are not isolated: each step commits on its own, so other requests can see, and act on, a saga's half-finished work.
Lost update: the coupon used twice
FIRST50 gives 50% off a customer's first order, once. A customer has the app open on their phone and their laptop and taps "Place order" on both within a second.
Saga A: check coupon FIRST50 for customer 9001 -> unused
Saga B: check coupon FIRST50 for customer 9001 -> unused
Saga A: mark coupon used
Saga B: mark coupon used
Both sagas read "unused" before either wrote "used". Both orders get 50% off. One saga's update has effectively been lost: the second one should have seen the first.
Dirty read: acting on something that gets undone
Restaurant 4417's kitchen can handle 30 open orders at once. The restaurant service rejects new orders when it's at 30.
Saga A: order 1 created as pending (count = 30)
Saga B: order 2 checks capacity -> 30, rejected
Saga A: payment declined, order 1 compensated (count = 29)
Saga B made a decision based on order 1, which never really existed. A customer was turned away from a restaurant that had room. That's a dirty read: reading another saga's uncommitted (in the business sense) work.
Fuzzy read: the value changed mid-saga
A saga reads the menu price of a dish in step 1 (₹320) and charges in step 3. Between those, the restaurant raises it to ₹350. Step 3 uses either the old price or the new one, depending on how it's coded, and the bill and the restaurant's payout might disagree.