Inspiration:
Payment bugs are some of the nastiest in software — they're rare, timing-dependent, and nearly impossible to reproduce on demand. Providers warn upfront that webhooks can arrive duplicated or out of order, and ACH returns can land days after the original transaction, long after handler code has moved on. Vendor sandboxes exist but are proprietary and hard to make misbehave intentionally. We wanted a vendor-neutral playground where you could deliberately break a payment handler, watch exactly how it fails, and hand someone else the precise failure as a link — something nothing we found quite did.
What it does / how we built it:
Rails Sim Playground is a deterministic, in-browser payments simulator with a virtual clock, a hand-rolled seeded PRNG, and a strict (time, seq)-ordered event queue — no wall clock, no real randomness, no floating-point money. A sim-core crate (pure Rust, zero async/I/O) runs the simulation; a thin Axum-based sim-api is the only place network concerns live; a React/TypeScript frontend drives it over a stateless HTTP API. You inject faults — duplicate, reorder, delay, drop, crash-restart — against naive vs. hardened handlers over a Money(i64) ledger with checked-arithmetic invariant checks, and any failing run can be shrunk down to a minimal reproduction and shared as a replay-by-seed link.
Challenges:
The hardest constraint was making determinism non-negotiable rather than aspirational — banning HashMap iteration, f64, and the rand crate anywhere it could leak into the trace hash, since any of those would make "same seed → same result" quietly false. Building a shrinker that reliably collapses a complex failing fault sequence down to the smallest reproduction (without losing the actual bug) was its own puzzle, as was getting the ACH state machine's timing right against real settlement/return semantics. The constant tension was staying disciplined about scope — card and ACH edge cases are nearly infinite, and the project only works as a portfolio piece if the core loop (inject → break → shrink → share) is rock solid before chasing breadth.
What we learned:
How much discipline true determinism demands at every layer — RNG, data structures, time, even hashing — and how a tight, well-typed domain model (ledger invariants encoded as types plus asserted ones) makes concurrency bugs easier to reason about than any amount of ad hoc testing.
Log in or sign up for Devpost to join the conversation.