<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: QuietMoose</title>
    <description>The latest articles on DEV Community by QuietMoose (@quietmoose).</description>
    <link>https://dev.to/quietmoose</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4001296%2F18d99318-5674-44f6-ab8f-6efdc2173e7e.png</url>
      <title>DEV Community: QuietMoose</title>
      <link>https://dev.to/quietmoose</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/quietmoose"/>
    <language>en</language>
    <item>
      <title>Monte Carlo game economy stress-testing on a solo budget — and the two-speed architecture that made it feel instant</title>
      <dc:creator>QuietMoose</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:56:18 +0000</pubDate>
      <link>https://dev.to/quietmoose/monte-carlo-game-economy-stress-testing-on-a-solo-budget-and-the-two-speed-architecture-that-made-3amk</link>
      <guid>https://dev.to/quietmoose/monte-carlo-game-economy-stress-testing-on-a-solo-budget-and-the-two-speed-architecture-that-made-3amk</guid>
      <description>&lt;h2&gt;
  
  
  The problem worth solving
&lt;/h2&gt;

&lt;p&gt;Game economy design happens in spreadsheets. A designer models a gem earn rate, a pull cost, a pity counter — all in isolation. The spreadsheet can't run 1,000 players through 90 days simultaneously. It can't show you that the whale cohort monetizes fine while minnows starve by day 45. It definitely can't show you what happens when someone asks &lt;em&gt;"can we make gems 20% cheaper?"&lt;/em&gt; and three interconnected systems cascade red.&lt;/p&gt;




&lt;h2&gt;
  
  
  The engine
&lt;/h2&gt;

&lt;p&gt;The sim engine is a pure Python + NumPy package called &lt;code&gt;loot_sim&lt;/code&gt;. The domain model is a directed graph: &lt;strong&gt;sources&lt;/strong&gt; (faucets), &lt;strong&gt;sinks&lt;/strong&gt; (drains), &lt;strong&gt;converters&lt;/strong&gt;, &lt;strong&gt;pools&lt;/strong&gt;, &lt;strong&gt;gates&lt;/strong&gt;, &lt;strong&gt;flow edges&lt;/strong&gt;, and &lt;strong&gt;modifier edges&lt;/strong&gt; (feedback loops that scale a target node's parameter each tick).&lt;/p&gt;

&lt;p&gt;The day loop is fully vectorized. State arrays are &lt;code&gt;float32&lt;/code&gt;, shaped &lt;code&gt;(runs, players)&lt;/code&gt;. A Python-level per-player loop is a bug. Benchmark gate: full sim in under 4 seconds at 2 GB — in practice, 1,000 runs × 1,000 players × 90 days runs well under that on a cold Lambda.&lt;/p&gt;

&lt;p&gt;Seeded RNG uses &lt;code&gt;default_rng(SeedSequence(seed)).spawn(runs)&lt;/code&gt; — same inputs always return identical bytes. That property is load-bearing for the two-speed architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  The two-speed architecture
&lt;/h2&gt;

&lt;p&gt;This is the pattern I'm most likely to reuse. The tension in any simulation UI: real-time feedback requires fast computation, meaningful statistical results require expensive computation. Most tools pick one. I didn't want to.&lt;/p&gt;

&lt;p&gt;Two separate engines, two separate speeds, clear labeling so the user always knows which they're looking at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Preview engine&lt;/strong&gt; — TypeScript, client-side, deterministic mean-flow (no randomness, no cohort splitting — just expected value through the graph). Runs on every keystroke and slider drag. Under 16 ms. Labeled PREVIEW.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full Monte Carlo&lt;/strong&gt; — fires on Lambda, debounced 800 ms after slider release, immediately on button press, after every NLP mutation. Fan chart bands (p5–p95) replace the preview when the result arrives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The user never waits for the number that matters — they just get it.&lt;/p&gt;

&lt;p&gt;Parity tests enforce the preview median stays within ±10% of the full sim p50 at days 30, 60, and 90. If the preview drifts, the test fails and I fix the preview — I never widen the tolerance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Single-Lambda constraint
&lt;/h2&gt;

&lt;p&gt;One Lambda: FastAPI + Mangum adapter + metering + Bedrock client + sim engine. One SAM template. One deploy command. Never touched the AWS console for infra changes.&lt;/p&gt;

&lt;p&gt;The constraint saved hours every week — no service discovery, no inter-service latency to debug, no IAM roles between components. Tradeoff: a slow sim blocks the process. At under 4 seconds and reserved concurrency 10, that never became a problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cache before Bedrock, always
&lt;/h2&gt;

&lt;p&gt;The NLP bar is the most visible feature and the most expensive. Strict cost order: cache → Bedrock → suggestion chips → client-only. Bedrock never gets called without checking the cache first.&lt;/p&gt;

&lt;p&gt;~42 parameterized prompt intents — regex + RapidFuzz fuzzy matching at ≥87 similarity — running in both the frontend bundle and the Lambda. Common queries resolve without a network call. Validated Bedrock results write through to DynamoDB so the cache improves with use. In testing, ~60% of prompts never hit Bedrock.&lt;/p&gt;

&lt;p&gt;The kill switch is one DynamoDB write. Set &lt;code&gt;flag#bedrock → enabled: false&lt;/code&gt; and all LLM spend stops in under 60 seconds. The product stays up on cached results, suggestion chips, sliders, and the preview engine. Designing for this forced the fallback tiers to actually work.&lt;/p&gt;




&lt;h2&gt;
  
  
  One mutation format, everywhere
&lt;/h2&gt;

&lt;p&gt;Sliders, NLP results, cached responses, and undo all emit the same ops list: &lt;code&gt;add_node&lt;/code&gt;, &lt;code&gt;update_node&lt;/code&gt;, &lt;code&gt;remove_node&lt;/code&gt;, &lt;code&gt;add_edge&lt;/code&gt;, &lt;code&gt;update_edge&lt;/code&gt;, &lt;code&gt;remove_edge&lt;/code&gt;, &lt;code&gt;update_cohort&lt;/code&gt;, &lt;code&gt;clarify&lt;/code&gt;. One &lt;code&gt;apply_ops&lt;/code&gt; function per runtime applies them — pinned by shared test vectors that run against both the TypeScript and Python implementations and must produce identical graphs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern worth stealing
&lt;/h2&gt;

&lt;p&gt;Users don't need the real answer instantly — they need something to look at while they wait, and they need it to be honest about what it is.&lt;/p&gt;

&lt;p&gt;If you're building anything with a simulation loop behind a real-time UI, the two-speed pattern works: a fast deterministic approximation on every input, a full expensive result on commit, parity tests to keep them honest, and clear labeling throughout so the user always knows which is which.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>gamedev</category>
      <category>python</category>
      <category>testing</category>
    </item>
    <item>
      <title>I made the database compute everything: building an SLA-credit system of record on Aurora PostgreSQL + Vercel</title>
      <dc:creator>QuietMoose</dc:creator>
      <pubDate>Wed, 24 Jun 2026 22:07:01 +0000</pubDate>
      <link>https://dev.to/quietmoose/i-made-the-database-compute-everything-building-an-sla-credit-system-of-record-on-aurora-290d</link>
      <guid>https://dev.to/quietmoose/i-made-the-database-compute-everything-building-an-sla-credit-system-of-record-on-aurora-290d</guid>
      <description>&lt;p&gt;&lt;em&gt;I built this on nights and weekends to learn a stack I keep making decisions about but rarely touch with my own hands. Sharing what I learned in case it's useful to anyone doing the same.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I'm a principal product manager, and I've spent more nights than I'd like on incident bridges — watching a service degrade in real time, then writing the document that goes to the customer afterward: here's what broke, here's how long, here's what we're doing so it doesn't happen again. Owning that accountability up close teaches you something the dashboards don't: the hardest question isn't "what happened" — it's "what do we owe, and can we prove it?"&lt;/p&gt;

&lt;p&gt;Building integrations is my actual job, which means I spend a lot of time thinking about the teams on the &lt;em&gt;other&lt;/em&gt; side of an outage — the people who have to turn an incident into a number a customer will accept. And the same pains kept showing up. Getting the data is a scavenger hunt across systems that were never built to agree. Asking "what if we'd classified this differently" means re-running everything by hand, so nobody does until they're forced to. When a customer disputes the number, "I think so" is the most honest answer anyone can give — on the one topic where you can least afford it: money owed. And when a contract or a severity turns out to have been wrong, correcting a credit you already settled is a mess nobody wants to touch.&lt;/p&gt;

&lt;p&gt;None of those are calculation problems. They're &lt;em&gt;proof&lt;/em&gt; problems. So on nights and weekends, I built a system of record designed to solve them.&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;Attest&lt;/strong&gt; — a system of record for the SLA credits a B2B company owes its customers. Not a calculator, not a monitoring dashboard. The thing that can answer, with a receipt, "how much do we owe this customer, and can you prove it?"&lt;/p&gt;

&lt;p&gt;Here's what I learned making PostgreSQL — on Amazon Aurora — the actual product, with Vercel as a thin layer in front of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem, briefly
&lt;/h2&gt;

&lt;p&gt;When a service misses its SLA, the customer is owed a credit. Sounds like arithmetic. It isn't. The number depends on: how long the outage &lt;em&gt;really&lt;/em&gt; lasted, which minutes the contract excludes (scheduled maintenance), how severe the incident was classified, which contract version was in force at the time, where the month's total downtime lands against a tiered schedule, and the customer's monthly charge. Those inputs live in five different systems that were never built to talk to each other. The number is one value; assembling it by hand takes days, and when a customer disputes it, nobody can prove it in the room.&lt;/p&gt;

&lt;p&gt;The interesting realization: the hard part isn't the math. It's the &lt;strong&gt;proof&lt;/strong&gt;. And proof is an architecture decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core decision: the app computes nothing
&lt;/h2&gt;

&lt;p&gt;Most apps treat the database as a place to store rows and do the real work in application code. I inverted that. In Attest, the Next.js layer on Vercel passes parameters, renders rows, and does &lt;strong&gt;no credit math at all&lt;/strong&gt; — no tier lookups, no severity weighting, no downtime subtraction. A credit lookup is literally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;compute_credit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The route returns that row as-is. Everything that produces the number lives in the database.&lt;/p&gt;

&lt;p&gt;Why bother? Because "the database computed it" is &lt;em&gt;verifiable&lt;/em&gt; in a way "the app computed it" never is. If the math lives in TypeScript scattered across handlers, proving a number is correct means auditing code paths. If it lives in one SQL function, the derivation is a single, inspectable source of truth. For a product whose entire value is defensibility, that's not a purity exercise — it's the feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where PostgreSQL earns its keep: range types
&lt;/h2&gt;

&lt;p&gt;The piece I'd never used before and now love: &lt;strong&gt;range and multirange types.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The "credited downtime" for an incident is the outage &lt;em&gt;minus&lt;/em&gt; the maintenance windows the contract excludes. That's set subtraction over time intervals — exactly what &lt;code&gt;tstzmultirange&lt;/code&gt; is for. The heart of the whole system is one expression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;tstzmultirange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ii&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;impact_window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;range_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maint_window&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ii&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;impact_window&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;tstzmultirange&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it left to right: take the incident's impact window as a multirange, subtract the union of every maintenance window &lt;em&gt;clipped to that incident&lt;/em&gt; (&lt;code&gt;*&lt;/code&gt; is range intersection, &lt;code&gt;range_agg&lt;/code&gt; folds the windows together, &lt;code&gt;-&lt;/code&gt; is multirange difference). What comes back is the non-contiguous set of minutes that actually count — maybe two separate segments with a 14-minute hole carved out of the middle. No loops, no manual interval-merging in app code, no off-by-one bugs reconstructing intervals by hand. The database models time intervals as first-class values, and the subtraction is one operator.&lt;/p&gt;

&lt;p&gt;This matters more than it looks, because that credited-minutes number feeds everything downstream: it sets the month's total downtime, which sets the uptime percentage, which determines the tier, which sets the dollar amount. A few minutes of error in the subtraction can move the total across a tier boundary and change the credit by thousands. Getting that exactly right — and having it reconcile end to end — was the part that took the most iteration.&lt;/p&gt;

&lt;p&gt;The payoff is the most dramatic moment in the product: toggle whether one 14-minute maintenance window is excluded, and a credit steps from &lt;strong&gt;$2,400 to $6,000&lt;/strong&gt; — a $3,600 swing — because those minutes push the month across the 99.0% line into a worse tier. The whole thing is a single judgment call about whether maintenance was scheduled, and the range math makes the consequence visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The right index for the job: GiST on ranges
&lt;/h2&gt;

&lt;p&gt;Overlap queries — "which impacts fall in this month?", "which maintenance windows touch this incident?" — are the access pattern this whole system runs on. So the range columns (incident windows, impact windows, maintenance windows, classification valid-time, contract effective-ranges) get &lt;strong&gt;GiST indexes&lt;/strong&gt;, the index type built for range/geometric overlap.&lt;/p&gt;

&lt;p&gt;The win: an overlap query resolves through a &lt;strong&gt;GiST index scan&lt;/strong&gt; rather than a sequential scan over the whole table — the access path that holds as incident history grows. &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; shows the planner choosing the index rather than reading every row. GiST is also doing double duty: &lt;code&gt;EXCLUDE USING gist&lt;/code&gt; constraints enforce that contract versions and maintenance windows can't overlap &lt;em&gt;at write time&lt;/em&gt;, so the data can't get into an inconsistent state in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-tenancy I didn't have to hand-roll: Row-Level Security
&lt;/h2&gt;

&lt;p&gt;This is the one that most changed how I think. Tenant isolation — making sure company A can never see company B's data — is usually app-layer logic: every query carries a &lt;code&gt;WHERE account_id = ?&lt;/code&gt; and you pray no handler forgets it.&lt;/p&gt;

&lt;p&gt;PostgreSQL &lt;strong&gt;Row-Level Security&lt;/strong&gt; moves that guarantee into the database. One policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.tenant_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;applied to every tenant-facing table, plus a read-only role (&lt;code&gt;attest_tenant&lt;/code&gt;) with no write grants. The app sets &lt;code&gt;app.tenant_id&lt;/code&gt; for the session and then queries normally — the policy filters automatically. Run a query as the wrong tenant and you get &lt;strong&gt;zero rows&lt;/strong&gt;, not an error and not a leak. Isolation isn't something the application promises; it's something the database enforces. As a PM who's sat through more than one "how do we guarantee tenant isolation" conversation, seeing it become a database property instead of a code-review discipline was genuinely clarifying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the record honest: append-only + signed certificates
&lt;/h2&gt;

&lt;p&gt;A "system of record" that can quietly edit a settled number isn't one. So mutation is blocked at the database level: triggers make settled credits immutable, and classifications and corrections are &lt;strong&gt;append-only&lt;/strong&gt; — you supersede by inserting a new row, never by updating or deleting. When a contract gets renegotiated and uploaded late (it happens), you can't rewrite history; you issue a &lt;strong&gt;signed correction&lt;/strong&gt; that links to the original, which stays on the record — superseded, not erased.&lt;/p&gt;

&lt;p&gt;Each credit also exports as an &lt;strong&gt;Ed25519-signed certificate&lt;/strong&gt;. The signature covers a canonical payload embedded in the document, and verification only needs the embedded public key — &lt;strong&gt;no database lookup required&lt;/strong&gt;. The artifact carries its own proof. Signing happens in the app layer with Node's &lt;code&gt;crypto&lt;/code&gt;, and the signing key lives in &lt;strong&gt;AWS Secrets Manager&lt;/strong&gt; — fetched at invocation and cached in the warm instance, so the raw private key is never a plaintext environment variable in the deployed function. (The key does pass through app memory at sign time; KMS's signing API would avoid that but doesn't support Ed25519.) The &lt;em&gt;record&lt;/em&gt; the certificate attests to is entirely the database's.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI part, kept honest: Amazon Bedrock suggests, a human decides
&lt;/h2&gt;

&lt;p&gt;Incident severity drives the credit, and classifying severity from a noisy signal is a judgment call — a good fit for a model, a terrible fit for a model acting alone. So I used &lt;strong&gt;Amazon Bedrock&lt;/strong&gt; to &lt;em&gt;suggest&lt;/em&gt; a severity from the incident signal, and made the human override the real, recorded decision. Both live in the append-only classification history: the AI suggestion and the human's call, side by side, with provenance. Bedrock never writes to the database — the app inserts its suggestion as one row; a human override is a separate row. AI suggests, a person decides, the record captures both. For anything that touches money owed, that felt like the only honest pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this stack, specifically
&lt;/h2&gt;

&lt;p&gt;The hackathon constraint was Vercel for the frontend and a designated AWS database for the backend. What surprised me is how well that "thin app, heavy database" shape fit the problem. Aurora PostgreSQL gave me the range math, the GiST scaling, the RLS isolation, and the append-only integrity &lt;strong&gt;out of the box&lt;/strong&gt; — these aren't libraries I bolted on, they're native database capabilities. Vercel made the front end a deploy-and-forget concern so I could spend my limited nights-and-weekends time on the part that mattered: the record. (The connection to Aurora uses short-lived IAM tokens via &lt;code&gt;rds-signer&lt;/code&gt;.) The "zero stack" wasn't a limitation I worked around — it was the architecture that made the core claim true.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually learned (as a PM)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The hard problem usually isn't the calculation; it's the inputs and the proof.&lt;/strong&gt; I'd have scoped this product wrong a year ago — I'd have prioritized the math and under-invested in defensibility. Defensibility is the product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pushing logic into the database is a real strategy, not just a preference.&lt;/strong&gt; "The database computed it, and here's the function" is a different &lt;em&gt;trust&lt;/em&gt; posture than "the app computed it." I'll ask different questions in design reviews now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The most trustworthy system isn't the one that's never wrong — it's the one that can make a wrong thing right, on the record.&lt;/strong&gt; An append-only ledger that issues honest corrections beats one that pretends settled numbers never change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I learned more about Postgres, AWS, and Vercel building this one thing on weekends than I would have from a quarter of reading docs. If you're in product and you keep making calls about systems you've never built — pick a real problem and build it. It changes how you see the work.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I created this content for the purposes of entering the H0 Hackathon. Built with Amazon Aurora (PostgreSQL) and Vercel. #H0Hackathon&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>database</category>
      <category>postgres</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
