Author here. Would love feedback and pushback. Is expand-and-contract how you'd approach migrations? And how do you handle the long tail of clients in the wild?
HN user
lirbank
https://www.starmode.dev/ | https://www.lirbank.com/
Right, that's a real concern with naive concurrent tests - you're at the mercy of timing and the test becomes flaky. That's exactly what the synchronization barrier solves: it forces both transactions to reach the critical point before either proceeds, so the race condition is guaranteed to occur on every run. No flakiness.
Fair concern about reaching inside systems - it's not something to do lightly. The hooks are designed to be minimal: production code never calls them, they only activate in tests. But the core point is narrower than the thread might suggest - the article isn't about whether to use atomic updates vs locks vs SERIALIZABLE. It's about when your code has operations that could race, how do you prove your handling actually works?
You're right, Postgres wraps every statement in an implicit transaction. The point of that first example is that the SELECT and UPDATE are in separate auto-committed transactions - there's no explicit transaction block wrapping both. So another connection can change the value between your SELECT and your UPDATE.
Hey, thanks for sharing this - these bugs are so easy to miss because everything works fine until you get real concurrent traffic. And yeah, the moment you have multiple instances, app-level mutexes can't save you.
Oh that is super cool. Great prior art to study in combo with Loom. Very excited to dig in - imagine if there was an easy-to-use data race tester where you didn't have to figure out the interleaving points up front? Just point it at your code and let it find them. Exciting.
Yeah, the more I think about it, the more exciting this idea gets. The walkthrough in the article shows exactly why - I intentionally (to later show why that is wrong) place the barrier between the SELECT and UPDATE, which deadlocks instead of triggering the race. Getting the placement right requires reasoning about where the critical interleaving point is. An exhaustive approach would surface both outcomes automatically: this placement deadlocks, this one exposes the bug, this one passes. That would remove the hardest part of writing these tests.
Good call, SERIALIZABLE is a strong option - it eliminates a whole class of bugs at the isolation level. The trade-off is your app needs to handle serialization failures with retry logic, which introduces its own complexity. That retry logic itself needs testing, and barriers work for that too. On randomized testing - that actually has the same limitation you mentioned about barriers: you need to know where to point it. And without coordination, the odds of two operations overlapping at exactly the wrong moment are slim. You'd need enormous pressure to trigger the race reliably, and even then a passing run doesn't prove much. Barriers make the interleaving deterministic so a pass actually means something.
Absolutely - if you can express the whole operation as a single atomic statement, that's the best outcome. No locks needed, no race to test for. The article is about what comes next: when the logic can't collapse into one query, how do you verify your concurrency handling actually works?
Nice - that's a good case for barriers too. When there's no row to SELECT FOR UPDATE against, you'd inject the barrier after acquiring the advisory lock and verify the second transaction blocks until the first commits.
Interesting! The barrier approach is more targeted: you specify the exact interleaving you want to test rather than exploring all of them. Trade-off is you need to know which interleavings matter, but you get deterministic tests that run against a real database instead of a simulated runtime. Exploring exhaustive interleaving testing against a real Postgres instance could be a fun follow-up - I'd be curious if it's practical.
Good point. The barrier pattern from the article applies to both approaches - whether you're using pessimistic locks or optimistic version checks, it's good to verify that the concurrency handling actually works. Barriers let you test that your version check correctly rejects the stale update, the same way they test that your lock prevents the race.
Here's a real-world example where atomic updates aren't an option - an order status transition that reads the current status from one table, validates the transition, and inserts into another:
await db().transaction(async (tx) => { await hooks?.onTxBegin?.();
const [order] = await tx.select().from(orders)
.where(eq(orders.id, input.id))
.for("update");
const [status] = await tx.select().from(orderStatuses)
.where(eq(orderStatuses.orderId, input.id))
.orderBy(desc(orderStatuses.createdAt))
.limit(1);
if (input.status === status.code)
throw new Error("Status already set");
await tx.insert(orderStatuses).values({ ... });
});You need the transaction + SELECT FOR UPDATE because the validation depends on current state, and two concurrent requests could both pass the duplicate check. The hooks parameter is the barrier injection point from the article - that's how you test that the lock actually prevents the race.
Fair point - atomic updates like SET salary = salary + 500 sidestep the race condition entirely for simple cases. The examples are intentionally simplified to isolate the concurrency behavior. The barrier pattern is more relevant when you have read-modify-write operations that involve application logic between the read and the write - those can't always collapse into a single UPDATE.
Agree. This is great though. Don't need a separate service to email to quickly email a bunch of ppl. Avoid building products around a closed platform.
How is this interesting for a coder community?
Worth reposting.
A thread with the author https://twitter.com/MikaelLirbank/status/1761557470395449375
I am taking a stab on bookkeeping software for devs running a business. But I am thinking "Hacker Worthy" could be it's own category of software. What other types of products needs to be redone hacker worthy?
Every time I do bookkeeping for our family business, I wish the bookkeeping software was more like VSCode, so I started building that. A bookkeeping app for small businesses that looks and feels like a code editor. And then I kept building... Is this a terrible idea?
I really don't like boilerplates. They're always outdated and usually include more than what I want. I also find it interesting to set up the same tools every few months so I can see what they new defaults are etc.
But lately I've found myself copying a lot of boilerplate config between projects and I can't remember which project has the "winning" or "latest" configuration. So I figured, what the heck, maybe it is time for a template.
Do you also hate repo templates? Why? What would make them lovable?
He is a co-founder so I would believe he had more than a fraction of (unvested) stonks. A co-founder that left within a year, and prolly dropped all his shares (cliff, again my assumption).
That implies he considered Remix a failure and left. Interesting.
Also, why did Kent C Dodds leave a while ago? I mean, not the official story, but the actual one? He became a co-founder and left within a year...
Is this really a good thing though? Sounds a bit more like a soft landing than a success story. I am not sure this is in the interest for the framework either. Michael and Ryan will stay for two years until their lockin is over and then resign and start building the next thing (a good thing).