DEV Community

Aoxuan Guo for Momen

Posted on

Why You Need ACID: The Four Silent Bugs That Ship Money Out the Door

By Yaokai Jiang, Founder & CEO, Momen Talk delivered at No-Code Week Frankfurt, June 19, 2026

Your App Works in the Demo. Then It Quietly Loses Money.

I gave this talk on June 19, 2026, at No-Code Week Frankfurt. It's about ACID — Atomicity, Consistency, Isolation, and Durability — which sounds like a computer science lecture, but I want to make it concrete through bugs you can feel. No definitions first. Stories first.

The scary part about these bugs is that they're silent. No red screen, no stack trace. Your app just slowly drifts between what it says is true and what's actually true. And the most insidious thing: they only hit you when you're successful. If you have two users, nobody cares. It's only when you start getting real traffic that the data starts going wrong in ways you can't even explain.

The Bugs: What They Look Like in the Wild

Bug #1 — Phantom Inventory

Imagine you build an inventory management system. Two buyers both check whether there's something left. They both click buy. Both of them think they bought it. There's an order under both of their accounts. But you only had one piece to sell. Now you're oversold.

The reason this happens: the check and the decrement weren't one indivisible step. Both buyers were told "1 left" before either of them had actually bought it. They raced through the same gap.

Bug #2 — The Missed Transfer

You have a transfer of money — adding money to one person's balance and subtracting it from the other. Both writes should happen. But in the middle of that, something goes wrong. You've already subtracted money from one account, and then your database crashes, or your server crashes, or — worst case — you're doing this on the frontend. So you have two requests: subtract happens, and right now the user just closes the tab. $50 gone from the sender. The recipient never got it. Money just vanished.

The Same Bug in More Disguises

There are other ways this surfaces. Two people are updating the status of the same order at the same time, but neither knows the other is operating on the same piece of data. One says it ships today. The other says it ships tomorrow. The last write wins. The first person just left — they thought it was shipping today. In the end it ships on Friday, but the first person never knew their change was overridden.

And then there's the negative balance: both users check that there's enough money to proceed, both subtract 20, but you only had 30 to begin with. You end up with a negative balance, which is not supposed to happen. But it did, because the check and the action weren't atomic.

All Four Are the Same Promise, Broken

These aren't four unrelated bugs. They're all the same underlying failure — not having ACID guarantees:

  • A — Atomicity: all steps commit, or none. Fixes the missed transfer.
  • C — Consistency: your rules always hold. Fixes the negative balance.
  • I — Isolation: concurrent users don't collide. Fixes phantom inventory and the overwrite.
  • D — Durability: once saved, it survives a crash. Fixes the vanishing "Saved!"

If you didn't know these guarantees exist, you either picked a backend that doesn't expose them to you — or you picked the right backend but are using it the wrong way. Even if all the mechanisms are there, you can still be hit by this.

Why This Bites No-Code Builders Hardest

This is why it hurts no-code the most, because you didn't know it was there. First of all, it's hard to trace logs if you've never read code. Reasoning about all the different states in a system just from logs is genuinely hard. So it's best that you have strong guarantees in the first place so you never even hit them.

And then there's the frontend problem. If you use vibe-coding tools — especially if you're running logic on the frontend — that's the worst case. You have multi-step flows running on each visitor's phone or browser, and you control none of it. Anybody can just close their tab between step one and step two, and step two never runs. There's no server to finish it or undo it. A backend crash is rare. Closing a tab is every user, all day.

This is also why AI-generated apps tend to break under real traffic — the code often looks right, but the guarantees aren't there, and nobody told the model to put them in.

The Live Demos (What I Showed on Stage)

I ran three experiments live with volunteers from the audience.

Demo 1 — Concurrent status update. We had a post with a status field, and three of us tried to update it to three different values simultaneously. What should happen: one person gets a success, everyone else gets a failure — because while you were looking at it, someone already made a change. The correct way to implement this: when you send the update, don't just say id = 1. You also say updated_at is exactly what it was when you loaded the record. If someone else has already changed it, the updated_at won't match, so your update hits zero records, and you know you've been beaten to it. The database is the arbiter.

Demo 2 — Liking a post. One of the conditions of liking a post: you can't like the same post twice. We all clicked simultaneously — same user, because we all logged in with the same account. The result: exactly one like, no duplicates. The reason it works is that "like" is modeled as a many-to-many relation between a person and a post, and there's a unique constraint on (post_id, liked_by_id). That constraint is enforced by the database. It can never be violated no matter how you hit it. You will never forget to enforce it, and no race condition can slip past it.

Demo 3 — Inventory oversell. I built this on Supabase to show exactly the failure mode. I had a single row in an inventory table, amount: 2. The edge function checks the amount and if it's greater than zero, subtracts 1. I ran five threads simultaneously trying to take inventory. What should happen: two succeed, three fail, result is zero. What actually happens: everyone reads 2, everyone subtracts, you end up at minus 3.

The fix is simple but it has to be in the right place. Instead of reading the amount in code and then deciding whether to subtract, you push the whole thing into a single SQL statement:

UPDATE inventory SET amount = amount - 1
WHERE name = 'thing' AND amount > 0
The database checks and decrements in one atomic step. No race condition can fit between them. I showed the same fix working in Momen's action flow and directly in Supabase — Supabase supports it fine. The problem isn't the platform, it's using an edge function to read, then decide, then write. That gap between the check and the write is where the bug lives.

The Rule: Put the Arbiter at the Database

Always make sure that when there's a gap between time-to-check and time-to-use, with multiple people operating, the arbiter is the database. You can always ask an AI to make sure that's happening.

There are platforms where you simply can't do this. If you're touching multiple elements in multiple steps with Firebase, you often can't. If you're using an edge function for a multi-step flow, even a basic one, you can't. MongoDB has some transaction support but doesn't have unique constraints — and unique constraints are one of the most overlooked tools you have. If you want to ensure something can only happen once, the best place to enforce that is not in the code. It's a unique constraint in the database. It will never be violated no matter how you hit it, and you will never forget it's there.

One Test, Tomorrow Morning

"When the last jacket sells twice — what stops it?"

If the answer is "my workflow checks stock first" — it doesn't. A check-first step in stateless automation can't win a concurrency race. The guarantee has to live in the database. Only a transaction wins that race.

Yaokai Jiang is the Founder & CEO of Momen. This post is adapted from his talk at No-Code Week Frankfurt, June 19, 2026.

Top comments (0)