<?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: Mykyta Tretynko</title>
    <description>The latest articles on DEV Community by Mykyta Tretynko (@boynextdoor95).</description>
    <link>https://dev.to/boynextdoor95</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%2F4009789%2F34409ddc-d1b8-432f-b56a-c42439d5fb82.jpg</url>
      <title>DEV Community: Mykyta Tretynko</title>
      <link>https://dev.to/boynextdoor95</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/boynextdoor95"/>
    <language>en</language>
    <item>
      <title>Building a sub-millisecond BIN risk-scoring engine in Go (lock-free reads + hot-reload)</title>
      <dc:creator>Mykyta Tretynko</dc:creator>
      <pubDate>Tue, 30 Jun 2026 15:33:52 +0000</pubDate>
      <link>https://dev.to/boynextdoor95/building-a-sub-millisecond-bin-risk-scoring-engine-in-go-lock-free-reads-hot-reload-1gcj</link>
      <guid>https://dev.to/boynextdoor95/building-a-sub-millisecond-bin-risk-scoring-engine-in-go-lock-free-reads-hot-reload-1gcj</guid>
      <description>&lt;p&gt;A card &lt;strong&gt;BIN&lt;/strong&gt; (the first 6–8 digits) tells you which bank issued a card. Most BIN APIs stop there — brand, type, country. I wanted one that returns a &lt;strong&gt;decision&lt;/strong&gt;: given a BIN, approve / review / decline — and &lt;em&gt;why&lt;/em&gt; — fast enough to call inline on a checkout.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;BillionCore&lt;/strong&gt;, a BIN risk-scoring engine in Go. Here's what's under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision, not just data
&lt;/h2&gt;

&lt;p&gt;For each BIN the engine returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an &lt;strong&gt;approve / review / decline&lt;/strong&gt; call with the reasons behind it,&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;risk score&lt;/strong&gt; + issuer data,&lt;/li&gt;
&lt;li&gt;and (for media-buying/affiliate use) &lt;strong&gt;rebill probability, refund/chargeback risk and projected LTV&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every response lists the signals that moved the score — the "why" matters as much as the verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hot path must be fast — and never block
&lt;/h2&gt;

&lt;p&gt;It runs inline with payment/traffic decisions, so reads have to be sub-millisecond and must never stall, even while data is updating.&lt;/p&gt;

&lt;p&gt;The approach: keep the whole ruleset in memory as an &lt;strong&gt;immutable snapshot&lt;/strong&gt;, swapped atomically. Reads are a single atomic load, no mutex (simplified):&lt;/p&gt;

&lt;p&gt;​&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Store&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;snap&lt;/span&gt; &lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pointer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Snapshot&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c"&gt;// immutable; rebuilt once per reload&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Read path — lock-free&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Reload path — build a fresh snapshot, swap in one atomic op&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;buildSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="c"&gt;// keep serving the previous snapshot on failure&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// readers never block, never see a half-built map&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;&lt;span class="s"&gt;```



No locks on reads, no torn reads during reload, and a failed reload just keeps the last good snapshot. (Basically RCU — read-copy-update.)

## Hot-reload + a clean data/control-plane split

The data (BIN rules, performance stats, valid keys) changes over time, but I didn't want the fast path depending on a DB or app:

- The **Go engine** is the data plane — stdlib only, in-memory, lock-free.
- A separate **control plane** (a Laravel app) owns keys/rules/billing and writes them to disk atomically (temp file → `&lt;/span&gt;&lt;span class="n"&gt;rename&lt;/span&gt;&lt;span class="s"&gt;`).
- The engine **hot-reloads** from disk on an interval (and on `&lt;/span&gt;&lt;span class="n"&gt;SIGHUP&lt;/span&gt;&lt;span class="s"&gt;` for the big CSV datasets).

The engine keeps serving even if the control plane is down. Eventually-consistent updates are fine for this data and keep the hot path dependency-free.

## Why stdlib-only

`&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="s"&gt;` + `&lt;/span&gt;&lt;span class="n"&gt;ServeMux&lt;/span&gt;&lt;span class="s"&gt;` + the standard library got me sub-ms responses with a tiny footprint and zero dependency churn. For a focused service, a framework would've added weight without buying anything.

## Try it

Live demo, type any BIN, no signup → **https://billioncore.tech/#demo**
On RapidAPI if you want to call it from code → https://rapidapi.com/billioncore-billioncore-default/api/bin-lookup-risk-scoring-billioncore

## Honest notes

It's early. The performance dataset is solid on major issuers/geos and thinner on long-tail BINs — and a "decision" is only as good as the data behind it. Building this in public, so I'd genuinely value feedback on the scoring, the API shape, and where the data looks off.

If you've built low-latency lookup services in Go — how do you handle hot-reloading large datasets? Always looking to sharpen this.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>go</category>
      <category>api</category>
      <category>fintech</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
