<?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: Aivars Kalvāns</title>
    <description>The latest articles on DEV Community by Aivars Kalvāns (@aivarsk).</description>
    <link>https://dev.to/aivarsk</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%2F367555%2Fb61510de-ac39-41f9-8c25-0d8de79e920e.jpg</url>
      <title>DEV Community: Aivars Kalvāns</title>
      <link>https://dev.to/aivarsk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aivarsk"/>
    <language>en</language>
    <item>
      <title>Running TigerBeetle without a control plane database. Part two.</title>
      <dc:creator>Aivars Kalvāns</dc:creator>
      <pubDate>Sat, 04 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/aivarsk/running-tigerbeetle-without-a-control-plane-database-part-two-3c0p</link>
      <guid>https://dev.to/aivarsk/running-tigerbeetle-without-a-control-plane-database-part-two-3c0p</guid>
      <description>&lt;p&gt;&lt;a href="https://aivarsk.com/2025/12/06/tigerbeetle-without-olgp-database1/" rel="noopener noreferrer"&gt;Part one&lt;/a&gt; and some articles in between.&lt;/p&gt;

&lt;p&gt;Transactions and transaction lifecycles are complicated. Not to go into the craziness of payment card systems, we can say that all transactions consist of multiple transfers for fees, increasing and checking various limits, and may have reversals.&lt;/p&gt;

&lt;p&gt;Here, a transaction is a collection of individual &lt;a href="https://docs.tigerbeetle.com/reference/transfer/" rel="noopener noreferrer"&gt;Transfers&lt;/a&gt;. Similar to accounts, transfers can be linked together by having the same UUID value in &lt;code&gt;user_data_128&lt;/code&gt;. All transactions’ transfers will have &lt;code&gt;tb.AccountFlags.LINKED&lt;/code&gt; in &lt;code&gt;flags&lt;/code&gt; (except the last transfer) to achieve atomic behavior. I will also put the UUID as the &lt;code&gt;id&lt;/code&gt; of the first transfer to enforce uniqueness and idempotency.&lt;/p&gt;

&lt;p&gt;I want to have my transfers arbitrary: that means no clever ID generation schema, and I might choose when to store some transfers of 0 and when to skip to avoid noise. Whenever I need to know the exact transfers and their IDs, I will use the &lt;a href="https://docs.tigerbeetle.com/reference/requests/lookup_transfers/" rel="noopener noreferrer"&gt;&lt;code&gt;lookup_transfers&lt;/code&gt;&lt;/a&gt; and find transfers by &lt;code&gt;user_data_128&lt;/code&gt;. I will recognize the kind of transfer by the value of &lt;code&gt;code&lt;/code&gt;. Now, about some more interesting features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage limits
&lt;/h3&gt;

&lt;p&gt;We want to have daily/weekly/monthly limits for certain transaction types. These are easily implemented using pending transfers with &lt;a href="https://docs.tigerbeetle.com/reference/transfer/#timeout" rel="noopener noreferrer"&gt;timeouts&lt;/a&gt;: you can decrease the available weekly limit with a pending transfer that times out after 7x24x60x60 seconds. Or if you want calendar limits instead of rolling limits, you can calculate seconds until the end of the current week. TigerBeetle will do it’s best to void the transfer once the timeout expires, but it might do it a second or two later. But it is not a problem: existing payment systems are not precise as well and a drift even of several minutes is acceptable. It gets a bit tricky when combined with two-phase transactions out of &lt;a href="https://docs.tigerbeetle.com/coding/two-phase-transfers/" rel="noopener noreferrer"&gt;two-phase transfers&lt;/a&gt;. You have to read all the transfers that were created within the transaction and post all except transfers used for limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reversals
&lt;/h3&gt;

&lt;p&gt;Some of the transactions might be reversed, and we must enforce idempotency to avoid reversing them twice. (I am ignoring partial reversals at this point). One way is to generate a unique reversal ID outside TigerBeetle and use it. But that leaves the enforcement of “transaction can be reversed once” to the caller. A better way is to create a dummy “reversed” transfer of 0 in the original transaction in the pending state (here with the code 42):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;transfers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;debit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;credit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransferFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LINKED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;debit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;credit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransferFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_transfers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transfers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once reversal arrives, the first thing is to post (or void) the “reversed” transfer and then proceed with the rest of transfers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;transfers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;pending_id&lt;/span&gt;&lt;span class="o"&gt;=*&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;transfer&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransferFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VOID_PENDING_TRANSFER&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransferFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LINKED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;debit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;credit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_transfers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transfers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TigerBeetle ensures that each transfer can be posted or voided exactly once, and we get our idempotency out of that. Repeated reversals will result with error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[CreateTransfersResult(index=0, result=&amp;lt;CreateTransferResult.PENDING_TRANSFER_NOT_PENDING: 26&amp;gt;), CreateTransfersResult(index=1, result=&amp;lt;CreateTransferResult.LINKED_EVENT_FAILED: 1&amp;gt;)]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Linked two-phase transfers
&lt;/h3&gt;

&lt;p&gt;There are two features that combine in a bit of a surprising way: &lt;a href="https://docs.tigerbeetle.com/coding/linked-events/" rel="noopener noreferrer"&gt;linked events&lt;/a&gt; and &lt;a href="https://docs.tigerbeetle.com/coding/two-phase-transfers/" rel="noopener noreferrer"&gt;two-phase transfers&lt;/a&gt;. We can create several linked transfers for them to behave atomically: either all succeed, or all fail. And we can utilize two-phase transfers to implement card transaction-like authorization requests and financial advice. We successfully created some linked transfers in pending state. Now to post or void them, each transfer has to be posted or voided individually: the linked transfers no longer behave as linked, although they still have linked flags stored. You can have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;both posted and pending transfers in the same linked batch initially&lt;/li&gt;
&lt;li&gt;some of the pending ones can be voided&lt;/li&gt;
&lt;li&gt;some of the pending ones can be posted&lt;/li&gt;
&lt;li&gt;some of the pending ones can stay pending&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It both helps in some cases (calendar limits) and requires extra work in others.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retries
&lt;/h3&gt;

&lt;p&gt;Let’s say we have a transaction that might fail with insufficient balance. And we follow the principles listed above and use the transaction UUID as the ID of the first transfer to enforce uniqueness and idempotency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;transfer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;debit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;credit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_transfers&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;CreateTransfersResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;=&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateTransferResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EXCEEDS_CREDITS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;54&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In some cases (subscriptions, fees, etc.), we consider an insufficient balance a transient error and want to retry later, when the customer might have a sufficient balance. But here’s the catch: repeating the same transfer with the same ID will complain about a previous failed(!) transfer already existing - &lt;a href="https://docs.tigerbeetle.com/reference/requests/create_transfers/#id_already_failed" rel="noopener noreferrer"&gt;it’s a part of the official documentation&lt;/a&gt;. This is not what I expected based on previous systems, but OK.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_transfers&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;CreateTransfersResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;=&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateTransferResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID_ALREADY_FAILED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now doing a lookup for that transfer ID with &lt;a href="https://docs.tigerbeetle.com/reference/requests/lookup_transfers/" rel="noopener noreferrer"&gt;lookup_transfers&lt;/a&gt; will return nothing - a transfer was not created, but it exists somehow anyway, like a ghost.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lookup_transfers&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="p"&gt;[]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Recipes vs real life
&lt;/h3&gt;

&lt;p&gt;TigerBeetle has several recipes that utilize pending transfers for doing conditional transfers like &lt;a href="https://docs.tigerbeetle.com/coding/recipes/balance-invariant-transfers/" rel="noopener noreferrer"&gt;balance invariant transfers&lt;/a&gt;. In real life, there are cases where you have to move money through some accounts for traceability or because the accountants said so. I think we first hit it when doing lower and upper bound checks at the same time. Here’s a simplified example: we want to transfer 1 from account A to B and then back to A. Works fine with the regular transfers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;transfers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;debit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;credit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransferFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LINKED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;debit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;credit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_transfers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transfers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we want to do the same with pending (because recipes use pending), but the second transfer fails and cancels the whole batch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;transfers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;debit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;credit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransferFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LINKED&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransferFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;debit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;credit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransferFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_transfers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transfers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;CreateTransfersResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;=&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateTransferResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LINKED_EVENT_FAILED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;CreateTransfersResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;=&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateTransferResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EXCEEDS_CREDITS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;54&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turns out balance checks do not use pending credits or debits, even for linked transfers. This breaks my brain, which is used to database transactions. But it kinda makes sense, knowing TigerBeetle implementation, that it’s possible to post or void individual transfers, and there is no way to express that some transfers will &lt;em&gt;always&lt;/em&gt; be either voided or posted atomically. Not a huge issue once you wrap your brain around it, but the recipes have to be implemented with regular transfers with manual cleanup, and all “technical” transfers stay in the posting history.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error reporting
&lt;/h3&gt;

&lt;p&gt;TigerBeetle reports just the error code and transfer index to the caller. To make a generic and future-proof error handling that does not rely on the ordering of transfers, we have a function that takes errors and transfers and figures out the correct error code by looking at the error code, transfers &lt;code&gt;code&lt;/code&gt; field and maybe account IDs. Sometimes &lt;code&gt;CreateTransferResult.EXCEEDS_CREDITS&lt;/code&gt; means there are insufficient funds, sometimes it means exceeded daily limits, sometimes the requested transaction amount is lower than the transaction fees.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;To be continued.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>distributedsystems</category>
      <category>fintech</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Running TigerBeetle without a control plane database. Locking.</title>
      <dc:creator>Aivars Kalvāns</dc:creator>
      <pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/aivarsk/running-tigerbeetle-without-a-control-plane-database-locking-3moa</link>
      <guid>https://dev.to/aivarsk/running-tigerbeetle-without-a-control-plane-database-locking-3moa</guid>
      <description>&lt;p&gt;Of course, TigerBeetle does not provide anything for locking, but I don’t want to introduce a control-plane database or add Redis to the mix, so let’s build some new nails for the TigerBeetle hammer.&lt;/p&gt;

&lt;p&gt;First, we will need a dummy account to use as the second leg for the double-entry accounting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_accounts&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we will need a new account for each lock (mutex) with &lt;code&gt;flags&lt;/code&gt; that prevent a negative balance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;mutex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccountFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEBITS_MUST_NOT_EXCEED_CREDITS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_accounts&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each mutex must be initialized with an initial balance of 1. To make it fail-proof, we can use the account identifier as the transfer identifier to enforce idempotency. We will get &lt;code&gt;CreateTransferResult.EXISTS&lt;/code&gt; when the mutex account has been initialized before.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;init&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;debit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;credit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_transfers&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;init&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;errors&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateTransferResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EXISTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can implement the actual locking. We will implement lock expiration (or Time-To-Live) to avoid crashed or stalled processes from halting the system. TigerBeetle has a &lt;a href="https://docs.tigerbeetle.com/reference/transfer/#timeout" rel="noopener noreferrer"&gt;timeout feature&lt;/a&gt; that will come in handy.&lt;/p&gt;

&lt;p&gt;Locking will transfer an amount of 1 from the mutex account back to the dummy account with a timeout that will automatically cancel the transfer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;debit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;credit_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransferFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TigerBeetle does not provide a tool to wait for the account balance, so we need a busy-loop that will try to acquire the lock for some time. We will retry the locking transfer when it fails with &lt;code&gt;CreateTransferResult.EXCEEDS_CREDITS&lt;/code&gt;. The curious detail is that the failing transfer is still persisted (?!) - we need to assign a new transfer ID for each try, or it will fail with an error saying that the transfer already exists. And to avoid polluting TigerBeetle with failed locking attempts, I added a small delay after each failure. But just imagine all our locking attempts stored durably for audit purposes :D&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_transfers&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;lock&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;errors&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;errors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateTransferResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EXCEEDS_CREDITS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now the easy part - releasing the lock is just voiding the pending lock transfer. The only interesting part here is handling the expired transfers - when you have held the lock for a longer time than the Time-To-Live. We might want to report these cases to better adjust the timeout values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;unlock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;pending_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransferFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VOID_PENDING_TRANSFER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_transfers&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;unlock&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;errors&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;errors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateTransferResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PENDING_TRANSFER_EXPIRED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is it crazy? Yes. But does it work? Yes.&lt;/p&gt;

</description>
      <category>database</category>
      <category>distributedsystems</category>
      <category>fintech</category>
      <category>python</category>
    </item>
    <item>
      <title>“AI” is the fast food of software development.</title>
      <dc:creator>Aivars Kalvāns</dc:creator>
      <pubDate>Sun, 21 Jun 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/aivarsk/ai-is-the-fast-food-of-software-development-58jh</link>
      <guid>https://dev.to/aivarsk/ai-is-the-fast-food-of-software-development-58jh</guid>
      <description>&lt;p&gt;Yes, it fills your stomach with calories quickly and cheaply. No, a few fast food meals will not kill you.&lt;/p&gt;

&lt;p&gt;And yes, you can stay healthy longer by hand-picking ingredients and declining each “Would you like to supersize that?”&lt;/p&gt;

&lt;p&gt;But pushing it so much is a bit strange: Look, I got my food in 3 minutes and didn’t even have to leave the car. The foodservice industry is cooked now.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>discuss</category>
      <category>programming</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>TigerBeetle as a file storage</title>
      <dc:creator>Aivars Kalvāns</dc:creator>
      <pubDate>Sun, 07 Dec 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/aivarsk/tigerbeetle-as-a-file-storage-540p</link>
      <guid>https://dev.to/aivarsk/tigerbeetle-as-a-file-storage-540p</guid>
      <description>&lt;p&gt;&lt;em&gt;Could not keep it under the rug until April Fool’s Day&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TigerBeetle is a reliable, fast, and highly available database for financial accounting. It tracks financial transactions &lt;strong&gt;or anything else that can be expressed as double-entry bookkeeping&lt;/strong&gt; , providing three orders of magnitude more performance and &lt;strong&gt;guaranteeing durability even in the face of network, machine, and storage faults.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgrzk9remn36sqkfhvelx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgrzk9remn36sqkfhvelx.png" alt="Challenge accepted" width="225" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Continuing my &lt;a href="https://aivarsk.com/2025/12/06/tigerbeetle-without-olgp-database1/" rel="noopener noreferrer"&gt;if all you have is a hammer, everything looks like a nail&lt;/a&gt; journey, I wanted to store arbitrary binary blobs in TigerBeetle to protect them from storage faults. If I can do that, I can store anything.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;id&lt;/code&gt; field of my Accounts will contain the filename (16-byte limit). I will store the total file size in the &lt;code&gt;user_data_64&lt;/code&gt; field and the filename length in the &lt;code&gt;user_data_32&lt;/code&gt; field (to simplify decoding). And my Accounts will have this nice property that &lt;code&gt;credits_posted&lt;/code&gt; will contain the actual number of bytes written. I can detect failed uploads and resume that (a future TODO).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
def create_a_file(filename, size):
    if len(filename) &amp;gt; 16:
        raise ValueError("Invalid filename, more than 16 bytes")
    account = tb.Account(
        id=int.from_bytes(filename.encode()),
        user_data_64=size,
        user_data_32=len(filename),
        ledger=FILE,
        code=FILE,
    )
    errors = client.create_accounts([account])
    if errors:
        raise ValueError(errors[0])
    return account

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What makes me unhappy is that I have not found a good use for &lt;code&gt;user_data_128&lt;/code&gt; on the Account record. Such a waste of resources!&lt;/p&gt;

&lt;p&gt;I will store the actual bytes in Transfer &lt;code&gt;user_data_128&lt;/code&gt;, &lt;code&gt;user_data_64&lt;/code&gt;, and &lt;code&gt;user_data_32&lt;/code&gt; fields. That gives a total of 28 bytes per Transfer, and the Transfer &lt;code&gt;amount&lt;/code&gt; will contain the number of bytes used in the Transfer. Which will be 28 for all Transfers except the last one containing the remaining bytes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
            transfers.append(
                tb.Transfer(
                    id=tb.id(),
                    debit_account_id=system_id,
                    credit_account_id=file_id,
                    amount=len(block),
                    user_data_128=int.from_bytes(block[:16]),
                    user_data_64=int.from_bytes(block[16:24]),
                    user_data_32=int.from_bytes(block[24:]),
                    ledger=FILE,
                    code=FILE,
                )
            )

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because TigerBeetle uses double-entry bookkeeping, I will transfer all bytes from a system file “.” (debit side) to the desired file (credit side). Which is extremely useful for audit purposes to verify that &lt;code&gt;debits_posted&lt;/code&gt; on the system file Account is the same as &lt;code&gt;credits_posted&lt;/code&gt; on all file Account records.&lt;/p&gt;

&lt;p&gt;As for getting data out of TigerBeetle, I can retrieve all credit Transfers for the specific Account. They are always correctly ordered by the &lt;code&gt;timestamp&lt;/code&gt; field as &lt;a href="https://docs.tigerbeetle.com/coding/time/#timestamps-are-totally-ordered" rel="noopener noreferrer"&gt;guaranteed by the TigerBeetle.&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
        timestamp_min = 0

        while True:
            transfers = client.get_account_transfers(
                tb.AccountFilter(
                    account_id=file_id, flags=tb.AccountFilterFlags.CREDITS, limit=BULK, timestamp_min=timestamp_min
                )
            )
            for transfer in transfers:
                timestamp_min = transfer.timestamp
                ...

            if len(transfers) &amp;lt; BULK:
                break
            timestamp_min += 1

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All that put together, I performed tests on some of the most valuable files I did not want to ever looose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
(venv)  du -b ~/Downloads/homework.mp4
104718755 /home/aivarsk/Downloads/homework.mp4
(venv)  time ./tbcp ~/Downloads/homework.mp4 tb:backup.mp4

real 2m3.697s
user 1m4.408s
sys 0m1.568s

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, you can store your files durably at speeds close to 642 kB/s. Now, let’s retrieve the file and store it on the disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
(venv)  time ./tbcp tb:backup.mp4 copy.mp4

real 0m47.588s
user 0m27.027s
sys 0m0.553s

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Downloading is around four times faster at 2,228 kB/s! And of course, I verified that not a single bit was lost during the round-trip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
(venv)  sha256sum ~/Downloads/homework.mp4
4ee75486c7c65a5c158f7f6b2ca6458195aa25b155b0688173b4b52583ce4cac /home/aivarsk/Downloads/homework.mp4
(venv)  sha256sum copy.mp4
4ee75486c7c65a5c158f7f6b2ca6458195aa25b155b0688173b4b52583ce4cac copy.mp4
(venv) 

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to store your valuable files, guaranteeing durability even in the face of network, machine, and storage faults, &lt;a href="https://gist.github.com/aivarsk/2b26854c956e36fdfd73349586f2b168" rel="noopener noreferrer"&gt;here is the full source code&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tigerbeetle</category>
      <category>gl</category>
      <category>accounting</category>
    </item>
    <item>
      <title>Running TigerBeetle without a control plane database. Part one.</title>
      <dc:creator>Aivars Kalvāns</dc:creator>
      <pubDate>Sat, 06 Dec 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/aivarsk/running-tigerbeetle-without-a-control-plane-database-part-one-16h4</link>
      <guid>https://dev.to/aivarsk/running-tigerbeetle-without-a-control-plane-database-part-one-16h4</guid>
      <description>&lt;p&gt;TigerBeetle is a database built for financial accounting, and the only record types available are &lt;a href="https://docs.tigerbeetle.com/reference/account/" rel="noopener noreferrer"&gt;Accounts&lt;/a&gt; and &lt;a href="https://docs.tigerbeetle.com/reference/transfer/" rel="noopener noreferrer"&gt;Transfers&lt;/a&gt;. That might be enough for the simplest accounting setup, but not for any realistic financial product.&lt;/p&gt;

&lt;p&gt;The way &lt;a href="https://docs.tigerbeetle.com/coding/system-architecture/" rel="noopener noreferrer"&gt;TigerBeetle solves that&lt;/a&gt; is by requiring an Online General Purpose (OLGP) database in the control plane that stores metadata and mapping between TigerBeetle’s identifiers and identifiers used by the rest of the systems. This can be done, and the documentation is really nice on guiding you, but… what about the dual write problem?&lt;/p&gt;

&lt;p&gt;Here’s an idea: what about running TigerBeetle without the control plane database? I am not saying you should do it, but I wanted to try out if that is possible and the best ways to do it. This is a work in progress.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge
&lt;/h3&gt;

&lt;p&gt;It depends on the banking and payment card system, but often your account or card is not just a single physical account record. It is a “product” and “product agreement” that links together multiple accounts, conditions, metadata, and other types of records, just to tell you what the current balance is. Years ago, I worked with something we called “analytical accounting.” We had separate accounts for purchases, cashout, refunds, credit/debit transfers, interest, and different kinds of fees. Something similar that you can achieve by analytical systems, but it was running inside the accounting system with the same consistency guarantees.&lt;/p&gt;

&lt;p&gt;Accounts and Transfers in TigerBeetle are immutable. You can change credit and debit amounts by posting transactions. You can also update Account &lt;code&gt;flags&lt;/code&gt; and set or unset &lt;code&gt;AccountFlags.CLOSED&lt;/code&gt; by &lt;a href="https://docs.tigerbeetle.com/coding/recipes/close-account/" rel="noopener noreferrer"&gt;posting a pending transfer or voiding it&lt;/a&gt;. In practice, you often have more statuses for accounts to accept credits while rejecting debit operations, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tools
&lt;/h3&gt;

&lt;p&gt;TigerBeetle gives us 3 fields where we can store arbitrary information for both Accounts and Transfers: &lt;code&gt;user_data_128&lt;/code&gt;, &lt;code&gt;user_data_64&lt;/code&gt;, &lt;code&gt;user_data_32&lt;/code&gt;, containing 16, 8, and 4 bytes of information. There are other fields like &lt;code&gt;ledger&lt;/code&gt; and &lt;code&gt;code&lt;/code&gt; that can be used for some things, but generally they should represent the ledger (and the currency) and &lt;code&gt;code&lt;/code&gt; to distinguish different account and transfer types. And there is one more field: the &lt;code&gt;id&lt;/code&gt; field itself (128 bits or 16 bytes), which gives us uniqueness checks out of the box.&lt;/p&gt;

&lt;p&gt;When your systems use integer IDs, it is a straightforward task. Most likely, you will have UUIDs, and it is easy to convert them to integers and back using Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; user_data_128 = uuid.uuid4().int
&amp;gt;&amp;gt;&amp;gt; uuid.UUID(int=user_data_128)
UUID('a1833b6a-a185-47aa-90ac-2f78979df3be')

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have textual codes and identifiers, you can even store those with some encoding scheme:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; user_data_32 = int.from_bytes("EUR".encode())
&amp;gt;&amp;gt;&amp;gt; user_data_32.to_bytes(3).decode()
'EUR'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Further, TigerBeetle provides &lt;a href="https://docs.tigerbeetle.com/reference/requests/lookup_accounts/" rel="noopener noreferrer"&gt;&lt;code&gt;lookup_accounts&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://docs.tigerbeetle.com/reference/requests/lookup_transfers/" rel="noopener noreferrer"&gt;&lt;code&gt;lookup_transfers&lt;/code&gt;&lt;/a&gt; to retrieve Accounts and Transfers by the &lt;code&gt;id&lt;/code&gt; field. And there are &lt;a href="https://docs.tigerbeetle.com/reference/requests/query_accounts/" rel="noopener noreferrer"&gt;&lt;code&gt;query_accounts&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://docs.tigerbeetle.com/reference/requests/query_transfers/" rel="noopener noreferrer"&gt;&lt;code&gt;query_transfers&lt;/code&gt;&lt;/a&gt; to query Accounts and Transfers by a combination of &lt;code&gt;user_data_128&lt;/code&gt;, &lt;code&gt;user_data_64&lt;/code&gt;, &lt;code&gt;user_data_32&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;p&gt;Dealing with 1:1 relations is easy, just put our UUID in the &lt;code&gt;id&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;1:n relations are harder. First, let’s use &lt;code&gt;user_data_128&lt;/code&gt; to store our UUID and link together multiple accounts by having the same value in that field. Second, there might be multiple cards and accounts for the same client UUID. For that, you can store the counter per client in the &lt;code&gt;user_data_32&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;You can find all cards and accounts of a client by running a query on the &lt;code&gt;user_data_128&lt;/code&gt; field. After choosing the desired card/account, you can retrieve the whole TigerBeetle account set for the card/account product agreement by running a query on &lt;code&gt;user_data_128&lt;/code&gt; and &lt;code&gt;user_data_32&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import os
import uuid
from enum import IntEnum

import tigerbeetle as tb

class Code(IntEnum):
    MAIN_ACCOUNT = 1
    LIMIT_ACCOUNT = 2
    EUR = 978
    STATUS = 9000

def register_new_account_agreement(client_id: uuid.UUID):
    with tb.ClientSync(cluster_id=0, replica_addresses=os.getenv("TB_ADDRESS", "3000")) as client:
        existing = client.query_accounts(
            tb.QueryFilter(user_data_128=client_id.int, ledger=Code.EUR, code=Code.MAIN_ACCOUNT, limit=100)
        )

        seq = (max(account.user_data_32 for account in existing) + 1) if existing else 1

        accounts = [
            tb.Account(
                id=tb.id(),
                user_data_128=client_id.int,
                user_data_32=seq,
                ledger=Code.EUR,
                code=Code.MAIN_ACCOUNT,
                flags=tb.AccountFlags.LINKED,
            ),
            tb.Account(
                id=tb.id(),
                user_data_128=client_id.int,
                user_data_32=seq,
                ledger=Code.EUR,
                code=Code.LIMIT_ACCOUNT,
            ),
        ]
        account_errors = client.create_accounts(accounts)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a race condition between finding the highest value of the sequence number and assigning it to an account. But that can be solved by running account creation from a single thread or by serialisation through locks in Redis or somewhere else.&lt;/p&gt;

&lt;p&gt;This solves the creation of agreements/account sets. But what about updates?&lt;/p&gt;

&lt;h3&gt;
  
  
  If all you have is a hammer, everything looks like a nail
&lt;/h3&gt;

&lt;p&gt;You can’t modify any of the Account fields, but you can post a Transfer of &lt;code&gt;0&lt;/code&gt; amount with no financial impact and store information in any of the Transaction’s user data fields. And to read back the current value, you can ask for the last Transfer of a specific type (&lt;code&gt;code&lt;/code&gt; field) by using the &lt;code&gt;limit=1&lt;/code&gt; and &lt;code&gt;tb.AccountFilterFlags.REVERSED&lt;/code&gt; flag. Like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        main, limit = accounts[0], accounts[1]
        transfer_errors = client.create_transfers(
            [
                tb.Transfer(
                    id=tb.id(),
                    debit_account_id=limit.id,
                    credit_account_id=main.id,
                    amount=0,
                    ledger=main.ledger,
                    user_data_128=0b1001001,
                    code=Code.STATUS,
                )
            ]
        )
        ...

        transfers = client.get_account_transfers(
            tb.AccountFilter(
                account_id=main.id,
                limit=1,
                code=Code.STATUS,
                flags=tb.AccountFilterFlags.CREDITS | tb.AccountFilterFlags.REVERSED,
            ),
        )
        print(transfers[0].user_data_128)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When one Transfer has too little fields, you can always post two, three or more and retrieve the same amount to reconstruct the information. Not that you should do it, but if two extra fields are the only reason to introduce an OLGP database, you might choose to abuse TigerBeetle to achieve the same.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;To be continued about building a transaction out of multiple Transfers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tigerbeetle</category>
      <category>accounting</category>
      <category>gl</category>
    </item>
    <item>
      <title>The lost art of semaphores</title>
      <dc:creator>Aivars Kalvāns</dc:creator>
      <pubDate>Thu, 09 Oct 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/aivarsk/the-lost-art-of-semaphores-gl8</link>
      <guid>https://dev.to/aivarsk/the-lost-art-of-semaphores-gl8</guid>
      <description>&lt;p&gt;I am a huge fan of &lt;a href="https://man7.org/linux/man-pages/man7/svipc.7.html" rel="noopener noreferrer"&gt;System V Inter Process Communication primitives&lt;/a&gt;. There is some rawness and UNIX spirit to them. There is a newer and kinda “improved” version of those primitives named POSIX IPC. While there are a few things in POSIX IPC that can’t be done with System V IPC, most of the time it’s the other way around. Primarily due to the rawness of System V IPC. Let’s check the &lt;a href="https://man7.org/linux/man-pages/man7/sem_overview.7.html" rel="noopener noreferrer"&gt;POSIX semaphores&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sem_post&lt;/code&gt; can be used to release a semaphore (increment by 1)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sem_wait&lt;/code&gt; can be used to acquire a semaphore (and decrement by 1)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;System V IPC has a single call for that: &lt;a href="https://man7.org/linux/man-pages/man2/semop.2.html" rel="noopener noreferrer"&gt;&lt;code&gt;semop&lt;/code&gt;&lt;/a&gt;. It can increment or decrement a semaphore by an arbitrary value. It also has the operation flags for each operation. And there, within flags, you can find one of the pearls of System V IPC - the &lt;code&gt;SEM_UNDO&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;What the &lt;code&gt;SEM_UNDO&lt;/code&gt; flag does is add the operation to an &lt;a href="https://github.com/torvalds/linux/blob/50c19e20ed2ef359cf155a39c8462b0a6351b9fa/ipc/sem.c#L2415" rel="noopener noreferrer"&gt;“undo list” within the kernel&lt;/a&gt;. Whenever the process terminates because of natural causes or is brutally killed by &lt;code&gt;SIGTERM&lt;/code&gt;, &lt;code&gt;SIGKILL&lt;/code&gt;, out-of-memory killer, or other reasons, the kernel will revert the semaphore operation. Think about it - your process acquires a semaphore and gets killed while holding it, and it will prevent other processes from acquiring the semaphore again. With &lt;code&gt;SEM_UNDO&lt;/code&gt;, you can choose what happens: if you used the semaphore as a counting semaphore, you can ask the kernel to release it automatically. When you acquire the semaphore to modify some shared resources, you can keep the semaphore stuck. It’s all up to you.&lt;/p&gt;

&lt;p&gt;Which brings me back to a previous topic of &lt;a href="https://aivarsk.com/2025/08/26/gunicorn-busy-workers/" rel="noopener noreferrer"&gt;tracking Gunicorn’s busy worker count&lt;/a&gt;. I used a semaphore there as a “reverse counting semaphore”: I released the semaphore (increment by 1) every time a process starts and acquired the semaphore (decrement by 1) every time a process stopped. But Python’s &lt;code&gt;multiprocessing.Semaphore&lt;/code&gt; is a POSIX semaphore. When a worker gets killed by the OOM killer or dies, the semaphore is not decremented, and the worker count is incorrect.&lt;/p&gt;

&lt;p&gt;So I decided to build my own Python wrappers around the &lt;a href="https://github.com/aivarsk/sysvipc-python" rel="noopener noreferrer"&gt;System V IPC&lt;/a&gt; to fix this issue and also make my other System V IPC projects more enjoyable. It’s more fun to use Python for quick tests than C++ code. With the library, here’s how you count the Gunicorn’s busy workers.&lt;/p&gt;

&lt;p&gt;First, we have to create a new semaphore. It’s just a number that can be shared with others. The downside of that - you have to perform a manual cleanup by scheduling the removal of the semaphore when the main process terminates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import atexit
import sysvipc

sem = sysvipc.semget(sysvipc.IPC_PRIVATE, 1, 0o600)
atexit.register(lambda: sysvipc.semctl(sem, 0, sysvipc.IPC_RMID))

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can read the current value of the semaphore at any time with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curval = sysvipc.semctl(sem, 0, sysvipc.GETVAL)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then each process can increment the semaphore without confusing the reader with strange names like “unlock” or “post”. I also specify the &lt;code&gt;SEM_UNDO&lt;/code&gt; flag, and the kernel will apply &lt;code&gt;-1&lt;/code&gt; to the semaphore when the process terminates for any reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sysvipc.semop(sem, [(0, 1, sysvipc.SEM_UNDO)])

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the process is done with the work, I decrement the semaphore. Again - no confusing names like “lock” or “wait”. The &lt;code&gt;SEM_UNDO&lt;/code&gt; will add &lt;code&gt;+1&lt;/code&gt; to the kernel’s semaphore adjustments and make the total adjustment &lt;code&gt;0&lt;/code&gt;. Past this point, when a process terminates, nothing will be subtracted from the semaphore value, and it will correctly represent the number of active workers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sysvipc.semop(sem, [(0, -1, sysvipc.SEM_UNDO)])

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is just the beginning, I need to write more &lt;a href="http://pybind11.com/" rel="noopener noreferrer"&gt;Pybind11&lt;/a&gt; wrappers for System V IPC to unlock more goodies in Python.&lt;/p&gt;

</description>
      <category>python</category>
      <category>unix</category>
      <category>semaphore</category>
      <category>gunicorn</category>
    </item>
    <item>
      <title>Talking to payment cards over NFC</title>
      <dc:creator>Aivars Kalvāns</dc:creator>
      <pubDate>Sun, 28 Sep 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/aivarsk/talking-to-payment-cards-over-nfc-56i7</link>
      <guid>https://dev.to/aivarsk/talking-to-payment-cards-over-nfc-56i7</guid>
      <description>&lt;p&gt;I had a great experience speaking about contactless payment cards at &lt;a href="https://bsideskrakow.pl/" rel="noopener noreferrer"&gt;BSides Krakow&lt;/a&gt;. For those who want to get their hands dirty:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://drive.google.com/file/d/1sgm-PzaBobW9gHJ9kOBxmA0oexW6LaxW/view?usp=sharing" rel="noopener noreferrer"&gt;Slides are here&lt;/a&gt; and &lt;a href="https://gist.github.com/aivarsk/4eb5d1756b36989cde2c38ac4b95c050" rel="noopener noreferrer"&gt;here are the code snippets&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cards</category>
      <category>nfc</category>
      <category>emv</category>
      <category>hce</category>
    </item>
    <item>
      <title>Ring buffer in the database</title>
      <dc:creator>Aivars Kalvāns</dc:creator>
      <pubDate>Tue, 23 Sep 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/aivarsk/ring-buffer-in-the-database-4n81</link>
      <guid>https://dev.to/aivarsk/ring-buffer-in-the-database-4n81</guid>
      <description>&lt;p&gt;We had a requirement to display the last N transactions on the ATM screen (”mini-statement”). The simplest solution is to keep the list of transactions, order them by date, and take the newest N transactions. But it gets tricky once you realize there are active customers making several transactions per day and inactive ones who use the card occasionally and might make a transaction every couple of months. Which means you have to preserve transaction records for a long period, and queries run more slowly. For a larger customer base, even half a year of data might be challenging to query in real time.&lt;/p&gt;

&lt;p&gt;Now we start thinking of keeping only the last N transactions per payment card and doing an &lt;code&gt;INSERT&lt;/code&gt; followed by a clever &lt;code&gt;DELETE&lt;/code&gt; that discards all extra transactions. In a regular programming language, a better solution would be to have a &lt;a href="https://en.wikipedia.org/wiki/Circular_buffer" rel="noopener noreferrer"&gt;ring buffer&lt;/a&gt; with a fixed capacity and a “write pointer” that points to where the newest record should be stored, overwriting the oldest one. To implement something similar in the database, we would need locking to prevent concurrent updates between retrieving the “write pointer”, storing the record, and advancing the “write pointer”.&lt;/p&gt;

&lt;p&gt;Years ago, I came up with a solution that I still have mixed feelings about. It is nice because it can be done with a single SQL statement, requires no explicit locking, and it works (in production as well). On the other hand, it is a bit fugly, my internal DBA demon complains about the size of the WAL records, and it’s weird. Old colleagues called that “shifter” because it works like the bit-shifting operations &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; and &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First, you create a model with as many message/transaction fields as you want to keep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class History(models.Model):
    ...
    message1 = models.TextField(blank=True, null=True)
    message2 = models.TextField(blank=True, null=True)
    message3 = models.TextField(blank=True, null=True)
    message4 = models.TextField(blank=True, null=True)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time you want to store a new message, you assign it to the field &lt;code&gt;message1&lt;/code&gt;. Whatever value was there in &lt;code&gt;message1&lt;/code&gt; you store it in &lt;code&gt;message2&lt;/code&gt;. Whatever value was there in &lt;code&gt;message2&lt;/code&gt;you store it in &lt;code&gt;message3&lt;/code&gt; , etc. And the value of the last field &lt;code&gt;message4&lt;/code&gt; in this case is forgotten.&lt;/p&gt;

&lt;p&gt;And here’s how it works. We start with an empty ring buffer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; History.objects.get(pk=1). __dict__
{'_state': &amp;lt;django.db.models.base.ModelState object at 0x721f90e8bf50&amp;gt;, 'id': 1, 'message1': None, 'message2': None, 'message3': None, 'message4': None}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We add the first message and verify it is stored correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; History.objects.filter(pk=1).update(message4=F("message3"), message3=F("message2"), message2=F("message1"), message1="first message")
&amp;gt;&amp;gt;&amp;gt; History.objects.get(pk=1). __dict__
{'_state': &amp;lt;django.db.models.base.ModelState object at 0x721f90e8bfe0&amp;gt;, 'id': 1, 'message1': 'first message', 'message2': None, 'message3': None, 'message4': None}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We add the second message and verify it is stored correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; History.objects.filter(pk=1).update(message4=F("message3"), message3=F("message2"), message2=F("message1"), message1="second message")
&amp;gt;&amp;gt;&amp;gt; History.objects.get(pk=1). __dict__
{'_state': &amp;lt;django.db.models.base.ModelState object at 0x721f90e8bfb0&amp;gt;, 'id': 1, 'message1': 'second message', 'message2': 'first message', 'message3': None, 'message4': None}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We add the third message and verify it is stored correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; History.objects.filter(pk=1).update(message4=F("message3"), message3=F("message2"), message2=F("message1"), message1="third message")
&amp;gt;&amp;gt;&amp;gt; History.objects.get(pk=1). __dict__
{'_state': &amp;lt;django.db.models.base.ModelState object at 0x721f90ea8110&amp;gt;, 'id': 1, 'message1': 'third message', 'message2': 'second message', 'message3': 'first message', 'message4': None}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We add the fourth message and verify it is stored correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; History.objects.filter(pk=1).update(message4=F("message3"), message3=F("message2"), message2=F("message1"), message1="fourth message")
&amp;gt;&amp;gt;&amp;gt; History.objects.get(pk=1). __dict__
{'_state': &amp;lt;django.db.models.base.ModelState object at 0x721f90ea81a0&amp;gt;, 'id': 1, 'message1': 'fourth message', 'message2': 'third message', 'message3': 'second message', 'message4': 'first message'}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We add the fifth message and verify it is stored correctly and the first message has been discarded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; History.objects.filter(pk=1).update(message4=F("message3"), message3=F("message2"), message2=F("message1"), message1="fifth message")
&amp;gt;&amp;gt;&amp;gt; History.objects.get(pk=1). __dict__
{'_state': &amp;lt;django.db.models.base.ModelState object at 0x721f90ea82f0&amp;gt;, 'id': 1, 'message1': 'fifth message', 'message2': 'fourth message', 'message3': 'third message', 'message4': 'second message'}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So? Is this stupid or smart? After all these years I still have mixed feelings about it.&lt;/p&gt;

</description>
      <category>orm</category>
      <category>django</category>
      <category>database</category>
    </item>
    <item>
      <title>Tracking Gunicorn's busy worker count</title>
      <dc:creator>Aivars Kalvāns</dc:creator>
      <pubDate>Tue, 26 Aug 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/aivarsk/tracking-gunicorns-busy-worker-count-3g0i</link>
      <guid>https://dev.to/aivarsk/tracking-gunicorns-busy-worker-count-3g0i</guid>
      <description>&lt;p&gt;I was investigating performance issues of a Django application running with Gunicorn behind a Nginx server. First, I added more timing information to Nginx access.log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;log_format timing '$remote_addr - $remote_user [$time_local] '
                  '"$request" $status $body_bytes_sent '
                  '"$http_referer" "$http_user_agent" rt="$request_time" uct="$upstream_connect_time" uht="$upstream_header_time" urt="$upstream_response_time"';

access_log /var/log/nginx/access.log timing;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the Nginx reload, it started to report the total request time and the time waiting for a response from Gunicorn. I also checked the timing in Chrome developer tools. All the times matched, which means the network latency or Nginx was not to blame.&lt;/p&gt;

&lt;p&gt;However, for a specific URL, response times were in the range of 3 to 22 seconds. The Gunicorn access log already contained the time spent processing the request by using &lt;a href="https://docs.gunicorn.org/en/stable/settings.html#access-log-format" rel="noopener noreferrer"&gt;a custom access log format string&lt;/a&gt;. And within the Gunicorn, those URL requests took less than a second. It was clear that requests get buffered between Nginx and Gunicorn in the &lt;a href="https://docs.gunicorn.org/en/stable/settings.html#backlog" rel="noopener noreferrer"&gt;connection backlog&lt;/a&gt;, and Gunicorn needs &lt;a href="https://docs.gunicorn.org/en/stable/settings.html#worker-processes" rel="noopener noreferrer"&gt;more workers to process the requests&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But how to find out how many Gunicorn workers are being used, how many are idle, and how to monitor that? I did not find a good answer. However, Gunicorn has functions that are called &lt;a href="https://docs.gunicorn.org/en/stable/settings.html#pre-request" rel="noopener noreferrer"&gt;before a request is processed by the worker&lt;/a&gt; and &lt;a href="https://docs.gunicorn.org/en/stable/settings.html#post-request" rel="noopener noreferrer"&gt;after it has been processed&lt;/a&gt;. I could use that to maintain the total number of active requests. But how to do that across multiple processes and avoid the lost updates?&lt;/p&gt;

&lt;p&gt;Meet the &lt;a href="https://docs.gunicorn.org/en/stable/settings.html#access-log-format" rel="noopener noreferrer"&gt;multiprocess Semaphore&lt;/a&gt;! It is a number living in the OS kernel memory: acquiring a semaphore decrements its value, releasing a semaphore increments the value, and a semaphore can’t become negative (acquiring will block). Normally, it is used for synchronization, but I will use it as an atomic gauge: release to increase it and acquire to decrease it.&lt;/p&gt;

&lt;p&gt;Another trick I discovered: the Gunicorn access log can log the request headers. So instead of adding custom logs, I added a new HTTP header to the request and stored the counter value in it. And here is the complete solution for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from multiprocessing import Semaphore

accesslog = "-"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" rt=%(L)s busy=%({x-busy}i)s'

busy = Semaphore(0)

def pre_request(worker, req):
    busy.release()
    req.headers.append(("x-busy", str(busy.get_value())))

def post_request(worker, req, environ, resp):
    busy.acquire()

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>gunicorn</category>
      <category>django</category>
    </item>
    <item>
      <title>Monolith First</title>
      <dc:creator>Aivars Kalvāns</dc:creator>
      <pubDate>Tue, 08 Jul 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/aivarsk/monolith-first-15ph</link>
      <guid>https://dev.to/aivarsk/monolith-first-15ph</guid>
      <description>&lt;p&gt;Many go to Martin Fowler for microservice architecture, distributed systems, micro frontends, event sourcing, and other fancy ideas about architecture, but &lt;a href="https://martinfowler.com/bliki/MonolithFirst.html" rel="noopener noreferrer"&gt;a few have noticed the advice to do a monolith first&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As I hear stories about teams using a microservices architecture, I’ve noticed a common pattern.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Almost all the successful microservice stories have started with a monolith that got too big and was broken up&lt;/li&gt;
&lt;li&gt;Almost all the cases where I’ve heard of a system that was built as a microservice system from scratch, it has ended up in serious trouble.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This pattern has led many of my colleagues to argue that &lt;strong&gt;you shouldn’t start a new project with microservices, even if you’re sure your application will be big enough to make it worthwhile.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>monolith</category>
      <category>microservices</category>
    </item>
    <item>
      <title>Transactional task outbox in Django with django-taskq</title>
      <dc:creator>Aivars Kalvāns</dc:creator>
      <pubDate>Tue, 01 Jul 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/aivarsk/transactional-task-outbox-in-django-with-django-taskq-4801</link>
      <guid>https://dev.to/aivarsk/transactional-task-outbox-in-django-with-django-taskq-4801</guid>
      <description>&lt;p&gt;We have given up on distributed transactions (2PC) but have not given up working with multiple resources like the database, message brokers, and queues. Instead, everybody tries to build their own atomic operations over multiple resources. Some do code&amp;amp;pray, and others try to have a database as a source of truth and with &lt;a href="https://microservices.io/patterns/data/transactional-outbox.html" rel="noopener noreferrer"&gt;the transactional outbox pattern&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/django-taskq/" rel="noopener noreferrer"&gt;django-taskq&lt;/a&gt; uses the Django database as the one and only backend for storing tasks (function calls with parameters). Because it uses the Django ORM under the hood, it also obeys Django transactions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django_taskq.celery import shared_task

@shared_task(queue="kafka-events", autoretry_for=(Exception,))
def something_happened(*, key: str, value: str):
    ...

with transaction.atomic():
    model1.save()
    model2.save()
    something_happened.delay(key=str(uuid.uud4()), value=payload.model_dump_json())

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Either all models will be updated and a new task scheduled or none of it will happen. And when the task runs, the model changes will be there in the database. I guess all of us have a Celery story about having a similar code and tasks executed before changes are committed and visible in the database. At which point everybody starts using &lt;code&gt;transaction.on_commit&lt;/code&gt; that works most of the time while the broker keeps running, the network to broker is reliable and Redis or application is not being killed by OOM killer.&lt;/p&gt;

&lt;p&gt;It still does not prevent failures while the task is being executed or the task not being idempotent but at least both the model changes and the task is recorded atomically and task failure or success will be recorded in the database.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>A Philosophy of Software Design</title>
      <dc:creator>Aivars Kalvāns</dc:creator>
      <pubDate>Thu, 26 Jun 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/aivarsk/a-philosophy-of-software-design-2o5p</link>
      <guid>https://dev.to/aivarsk/a-philosophy-of-software-design-2o5p</guid>
      <description>&lt;p&gt;In a world full of Uncles Bob &lt;a href="https://www.youtube.com/watch?v=lz451zUlF-k" rel="noopener noreferrer"&gt;be John Ousterhout&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/johnousterhout/aposd-vs-clean-code" rel="noopener noreferrer"&gt;More of method length, comment and TDD discussion&lt;/a&gt; and &lt;em&gt;A Philosophy of Software Design&lt;/em&gt; should be on every bookshelf. It has &lt;a href="https://www.youtube.com/watch?v=4xqkI953K6Y" rel="noopener noreferrer"&gt;some critcism&lt;/a&gt; but so does everything.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
