Stripe processes payments as event chains, and in real-world applications, it’s important to validate that each payment follows the expected sequence. For example:
Expected Event Order:
- payment_created
- payment_captured
- payment_settled
- Optional:
payment_refunded
When events happen out of order, or some events are missing, it can indicate a broken payment chain, which can be costly in production systems.
In this post, we’ll walk through how to detect broken or incomplete payment chains using SQL Server with a real Stripe event example.
The Problem
We have a table called payment_events:
| payment_id | event_type | event_time |
|---|---|---|
| 101 | created | 2026-01-01 10:00 |
| 101 | captured | 2026-01-01 10:05 |
| 101 | settled | 2026-01-01 10:10 |
| 104 | created | 2026-01-02 09:00 |
| 104 | captured | 2026-01-02 09:05 |
| 104 | refunded | 2026-01-02 09:15 |
Here we can see:
- Payment 101: follows the correct order → valid payment
- Payment 104: refund happened without a settlement → broken chain
Our goal is to identify payment IDs with missing events or invalid transitions.
Step 1: Identify the Previous Event
To check the sequence of events, we use the LAG() window function. This lets us see the previous event for each payment:
PARTITION BY payment_idensures we check events within each payment.
ORDER BY event_timepreserves the correct timeline.
Now, we know the previous event for every row.
Step 2: Identify Missing Event Types
We can use CASE statements to flag if any required events are missing:
MAX(CASE …)returns 1 if the event exists, 0 otherwise.- Using
MAXinstead ofCOUNTis critical because Stripe may retry events — duplicates should not affect the validation.
✅ MAX checks existence, COUNT counts frequency, which can mislead in event pipelines.
Step 3: Identify Invalid Event Transitions
We also need to flag events that happen out of order:
- Any invalid transition gets flagged as 1, otherwise 0.
- Using
MAXensures that if even one invalid transition exists, the payment ID is flagged.
Step 4: Aggregate and Validate
Now we can aggregate results into a single record per payment:
This query returns all payment IDs with missing or out-of-order events.
Step 5: Why MAX + CASE Works
MAXensures existence is checked, ignoring duplicates.COUNTwould return the number of events, which is misleading if Stripe retries events.CASEallows multiple conditions to be checked cleanly:
- If the first condition is met, the case exits - This ensures invalid transitions are flagged immediately
✅ Result for Our Example
| payment_id |
|---|
| 104 |
- Payment 104 is flagged because the refund happened without a settlement.
- Payment 101 is valid and not flagged.
Bonus Tips
-
Use window functions like
LAGto check event order explicitly. - Use MAX + CASE for existence checks.
- Use SUM + CASE or COUNT + CASE only when you want to count duplicates.
Key Takeaways
- Stripe payments should follow a strict sequence.
LAG()helps track the previous event for each payment.MAX(CASE…)is better thanCOUNTfor event existence validation.- Always flag missing events or invalid transitions to catch broken payment chains.
💡 Practice This Yourself
If you found this useful, try applying the same logic to other event pipelines like:
-
Order shipments (
created → packed → shipped → delivered) - User actions (
signup → verify_email → first_login)
You can also get more hands-on practice in my SQL Interview Practice Workbook →
https://knowstar.gumroad.com/l/gyaous