<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Dennis Thanner</title>
        <link>https://dnnsthnnr.com</link>
        <description>Your blog description</description>
        <lastBuildDate>Wed, 05 Aug 2026 10:24:01 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Dennis Thanner</title>
            <url>https://dnnsthnnr.com/favicon.ico</url>
            <link>https://dnnsthnnr.com</link>
        </image>
        <copyright>All rights reserved 2026</copyright>
        <item>
            <title><![CDATA[Idempotent database inserts: Getting it right]]></title>
            <link>https://dnnsthnnr.com/blog/idempotent-database-inserts-getting-it-right</link>
            <guid>https://dnnsthnnr.com/blog/idempotent-database-inserts-getting-it-right</guid>
            <pubDate>Tue, 24 Dec 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Idempotence is a critical concept in software systems, ensuring that a given operation can be performed multiple times
without changing the system's state beyond the initial application. It's particularly important in database operations,
where duplicate inserts can wreak havoc on data integrity and lead to subtle, hard-to-debug errors.
When done right, idempotent inserts enable fault tolerance and consistency in the face of retries, failures,
and distributed system complexities.</p>
<h2>A common solution</h2>
<p>A common solution to idempotent database inserts looks as follows, using PostgreSQL as the database of choice:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">CREATE</span> <span class="token keyword">TABLE</span> idempotent_table <span class="token punctuation">(</span>
	id <span class="token keyword">SERIAL</span> <span class="token keyword">PRIMARY</span> <span class="token keyword">KEY</span><span class="token punctuation">,</span>
	<span class="token keyword">data</span> <span class="token keyword">VARCHAR</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span><span class="token punctuation">,</span>
	idempotency_key <span class="token keyword">VARCHAR</span> <span class="token keyword">UNIQUE</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">-- Insert a row</span>
<span class="token keyword">INSERT</span> <span class="token keyword">INTO</span> idempotent_table <span class="token punctuation">(</span><span class="token keyword">data</span><span class="token punctuation">,</span> idempotency_key<span class="token punctuation">)</span>
    <span class="token keyword">VALUES</span> <span class="token punctuation">(</span><span class="token string">'foo'</span><span class="token punctuation">,</span> <span class="token string">'action1'</span><span class="token punctuation">)</span>
	<span class="token keyword">ON</span> CONFLICT <span class="token punctuation">(</span>idempotency_key<span class="token punctuation">)</span> <span class="token keyword">DO</span> NOTHING<span class="token punctuation">;</span>

<span class="token comment">-- Retry of the operation above</span>
<span class="token keyword">INSERT</span> <span class="token keyword">INTO</span> idempotent_table <span class="token punctuation">(</span><span class="token keyword">data</span><span class="token punctuation">,</span> idempotency_key<span class="token punctuation">)</span>
    <span class="token keyword">VALUES</span> <span class="token punctuation">(</span><span class="token string">'foo'</span><span class="token punctuation">,</span> <span class="token string">'action1'</span><span class="token punctuation">)</span>
	<span class="token keyword">ON</span> CONFLICT <span class="token punctuation">(</span>idempotency_key<span class="token punctuation">)</span> <span class="token keyword">DO</span> NOTHING<span class="token punctuation">;</span>
</code></pre>
<p>We create a table and attach a unique non-nullable column <code>idempotency_key</code>, when writing the record to the table
we check if the operation is colliding on the unique constraint of the <code>idempotency_key</code> and in that case do nothing.</p>
<p>With this implementation, we increased the resilience of the system by allowing it to retry a given action.
But let's look a bit deeper into edge cases and the behavior of the proposed solution.</p>
<h2>The flaw</h2>
<p>Let’s imagine the following. At first, a record is inserted in the database with idempotency key <code>action1</code>:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">INSERT</span> <span class="token keyword">INTO</span> idempotent_table <span class="token punctuation">(</span><span class="token keyword">data</span><span class="token punctuation">,</span> idempotency_key<span class="token punctuation">)</span>
    <span class="token keyword">VALUES</span> <span class="token punctuation">(</span><span class="token string">'foo'</span><span class="token punctuation">,</span> <span class="token string">'action1'</span><span class="token punctuation">)</span>
    <span class="token keyword">ON</span> CONFLICT <span class="token punctuation">(</span>idempotency_key<span class="token punctuation">)</span> <span class="token keyword">DO</span> NOTHING<span class="token punctuation">;</span>
</code></pre>
<p>And then shortly after a new record should be inserted, but due to an error the same <code>idempotency_key</code> value is used.</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">INSERT</span> <span class="token keyword">INTO</span> idempotent_table <span class="token punctuation">(</span><span class="token keyword">data</span><span class="token punctuation">,</span> idempotency_key<span class="token punctuation">)</span>
  <span class="token keyword">VALUES</span> <span class="token punctuation">(</span><span class="token string">'bar'</span><span class="token punctuation">,</span> <span class="token string">'action1'</span><span class="token punctuation">)</span>
  <span class="token keyword">ON</span> CONFLICT <span class="token punctuation">(</span>idempotency_key<span class="token punctuation">)</span> <span class="token keyword">DO</span> NOTHING<span class="token punctuation">;</span>
</code></pre>
<p>The outcome of the two statements would be: Only the first insert statement would be saved in the database
and the second insert will execute successfully but will not write a second row.</p>
<p>The flaw is, that we assume that the presence of an idempotency key in the database guarantees the correctness of
the corresponding data. This oversimplification can lead to systems that quietly accumulate stale, incomplete, or even
incorrect records under the radar.
And as everyone knows: assuming makes an ass out of you and me.</p>
<h2>How to fix it? - The improved solution</h2>
<p>The solution to the problem is rather simple, just don't assume that the record you want to insert and the record
existing in the database are the same, but rather validate that they are. This can be achieved by building a utility function.
The example will be using Python and SQLAlchemy, but this can be adapted to your tech stack.</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">idempotent_insert</span><span class="token punctuation">(</span>con<span class="token punctuation">:</span> AsyncConnection<span class="token punctuation">,</span> insert_statement<span class="token punctuation">:</span> Insert<span class="token punctuation">,</span> idempotency_key_column<span class="token punctuation">:</span> Column<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">&gt;</span> Row<span class="token punctuation">:</span>
	<span class="token comment"># Perform insert</span>
    cursor <span class="token operator">=</span> <span class="token keyword">await</span> con<span class="token punctuation">.</span>execute<span class="token punctuation">(</span>
	    insert_statement<span class="token punctuation">.</span>on_conflict_do_nothing<span class="token punctuation">(</span>index_elements<span class="token operator">=</span><span class="token punctuation">[</span>idempotency_key_column<span class="token punctuation">]</span><span class="token punctuation">)</span>
    <span class="token punctuation">)</span>

	<span class="token comment"># If a record has been inserted, it is a new record and stop here</span>
	<span class="token keyword">if</span> cursor<span class="token punctuation">.</span>rowcount<span class="token punctuation">:</span>
        <span class="token keyword">return</span>

    values_to_insert<span class="token punctuation">:</span> <span class="token builtin">dict</span> <span class="token operator">=</span> <span class="token punctuation">{</span>k<span class="token punctuation">:</span> v<span class="token punctuation">.</span>value <span class="token keyword">for</span> k<span class="token punctuation">,</span> v <span class="token keyword">in</span> insert_statement<span class="token punctuation">.</span>_values<span class="token punctuation">.</span>items<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">}</span>

    <span class="token comment"># Retrieve the existing idempotency key record from the table</span>
    idempotency_key_value <span class="token operator">=</span> values_to_insert<span class="token punctuation">.</span>get<span class="token punctuation">(</span>idempotency_key_column<span class="token punctuation">.</span>name<span class="token punctuation">)</span>
    cursor <span class="token operator">=</span> <span class="token keyword">await</span> con<span class="token punctuation">.</span>execute<span class="token punctuation">(</span>
        select<span class="token punctuation">(</span>insert_statement<span class="token punctuation">.</span>table<span class="token punctuation">)</span><span class="token punctuation">.</span>where<span class="token punctuation">(</span>idempotency_key_column <span class="token operator">==</span> idempotency_key_value<span class="token punctuation">)</span>
    <span class="token punctuation">)</span>

    existing_record<span class="token punctuation">:</span> <span class="token builtin">dict</span> <span class="token operator">=</span> cursor<span class="token punctuation">.</span>fetchone<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>_mapping

    <span class="token comment"># Compare the record in the database with the record we wanted to insert,</span>
	<span class="token comment"># if column values are differentiating, they will populate the variable not_matching_columns with a set of tuples (colum, value)</span>
    <span class="token keyword">if</span> not_matching_columns <span class="token operator">:=</span> <span class="token builtin">set</span><span class="token punctuation">(</span>values_to_insert<span class="token punctuation">.</span>items<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token builtin">set</span><span class="token punctuation">(</span>
        existing_record<span class="token punctuation">.</span>items<span class="token punctuation">(</span><span class="token punctuation">)</span>
      <span class="token punctuation">)</span><span class="token punctuation">:</span>
      <span class="token keyword">raise</span> IdempotencyConflictError<span class="token punctuation">(</span>
        <span class="token string-interpolation"><span class="token string">f"To be inserted record does not match with existing record on the same idempotency "</span></span>
        <span class="token string-interpolation"><span class="token string">f"'</span><span class="token interpolation"><span class="token punctuation">{</span>idempotency_key_value<span class="token punctuation">}</span></span><span class="token string">'. Different columns: </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token string">', '</span><span class="token punctuation">.</span>join<span class="token punctuation">(</span><span class="token builtin">map</span><span class="token punctuation">(</span><span class="token keyword">lambda</span> x<span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">,</span> not_matching_columns<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">}</span></span><span class="token string">"</span></span>
      <span class="token punctuation">)</span>
</code></pre>
<p>With this implementation, a second usage of the same idempotency key leads to a full validation of the record to
be inserted is equal to the record existing in the database table:</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">async</span> <span class="token keyword">with</span> db_connection_pool<span class="token punctuation">.</span>begin<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">as</span> con<span class="token punctuation">:</span>
	<span class="token keyword">await</span> idempotent_insert<span class="token punctuation">(</span>
		con<span class="token punctuation">,</span>
		Insert<span class="token punctuation">(</span>idempotent_table<span class="token punctuation">)</span><span class="token punctuation">.</span>values<span class="token punctuation">(</span>idempotency_key<span class="token operator">=</span><span class="token string">'action'</span><span class="token punctuation">,</span> data<span class="token operator">=</span><span class="token string">'foo'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
		idempotent_table<span class="token punctuation">.</span>c<span class="token punctuation">.</span>idempotency_key
	<span class="token punctuation">)</span>

	<span class="token comment"># will raise IdempotencyConflictError pointing out that the entries differ in colum data</span>
	<span class="token keyword">await</span> idempotent_insert<span class="token punctuation">(</span>
		con<span class="token punctuation">,</span>
		Insert<span class="token punctuation">(</span>idempotent_table<span class="token punctuation">)</span><span class="token punctuation">.</span>values<span class="token punctuation">(</span>idempotency_key<span class="token operator">=</span><span class="token string">'action'</span><span class="token punctuation">,</span> data<span class="token operator">=</span><span class="token string">'bar'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
		idempotent_table<span class="token punctuation">.</span>c<span class="token punctuation">.</span>idempotency_key
	<span class="token punctuation">)</span>

</code></pre>
<h3>Caveats</h3>
<p>This solution is not a silver bullet and comes with a few caveats, that I need to point out.</p>
<p>Alternative solution: The issue can also be resolved by implementing a reconciliation, that reconciles elements in a
table e.g. for a given process A a record for A in the right shape is existing in the database. However, I found that
implementing an utility that checks the consistency right at runtime, is simpler and points out the problem faster than
implementing a reconciliation.</p>
<p>The proposed solution also works best in insert-only tables. As soon as modifications and updates come into the picture,
updated columns need to be excluded when comparing the inserting record to the existing record in the table.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Transactional Outbox: How to safely offload work into the background]]></title>
            <link>https://dnnsthnnr.com/blog/transactional-outbox-how-to-safely-offload-tasks-into-the-background</link>
            <guid>https://dnnsthnnr.com/blog/transactional-outbox-how-to-safely-offload-tasks-into-the-background</guid>
            <pubDate>Mon, 26 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Modern distributed systems often rely on asynchronous processing to scale operations and improve responsiveness.</p>
<p>While offloading this work to background jobs via message queues seems straightforward, the moment you introduce them alongside a relational database, you’re walking into a classic trap: the dual write problem.</p>
<p>In this article we explore the failure modes of naïve implementations, demonstrate the elegance of the Transactional Outbox pattern, and discuss how to scale it with modern tooling.</p>
<p>For the purpose of this article lets consider a basic e-commerce setup: where after the user places an order via an API call, the system then continues processing that by sending notifications and starting the order fulfilment.</p>
<img alt="An example flow of an ecommerce API, submitting the order, creating it, sending notification, and fulfillment" loading="lazy" width="1500" height="500" decoding="async" data-nimg="1" style="color:transparent" srcset="/_next/image?url=%2Fexample-ecommerce-service.png&amp;w=1920&amp;q=75 1x, /_next/image?url=%2Fexample-ecommerce-service.png&amp;w=3840&amp;q=75 2x" src="/_next/image?url=%2Fexample-ecommerce-service.png&amp;w=3840&amp;q=75">
<h2>Doing It Wrong: A Dual Write Dilemma</h2>
<p>A common, but incorrect approach looks like the following:</p>
<pre class="language-python"><code class="language-python"><span class="token decorator annotation punctuation">@router<span class="token punctuation">.</span>post</span><span class="token punctuation">(</span><span class="token string">'/orders'</span><span class="token punctuation">)</span>
<span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">create_order</span><span class="token punctuation">(</span>order<span class="token punctuation">)</span><span class="token punctuation">:</span>
	order_created <span class="token operator">=</span> <span class="token keyword">await</span> save_to_database<span class="token punctuation">(</span>order<span class="token punctuation">)</span>
	order_created_event <span class="token operator">=</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
	<span class="token keyword">await</span> publish_event<span class="token punctuation">(</span>order_created_event<span class="token punctuation">)</span>
	<span class="token keyword">return</span> order_created
</code></pre>
<p>The API endpoint accepts an incoming order, saves it in the database and publishes an event to Kafka or to a Redis queue and returns the response. A background worker picks up the event and continues with the email notification and the order fulfilment processing.</p>
<p>Seems innocent enough, but what happens if the database write succeeds and the event publishing fails?  You are left with an inconsistent state where your order is accepted but not processed.</p>
<p>A common suggestion to fix situation is: “Just commit to the database after you published the event”. Congratulations, you now simply reversed the failure case. If the database write fails, you have emitted an event for an order not existing in your system. Your workers are processing ghost orders!</p>
<h2>Using The Transactional Outbox Pattern</h2>
<p>The core problem of the proposed solution is the dual write. Writing into two systems as an atomic transaction is not possible in a non durable execution environment like an API call.</p>
<p>The solution is simple - avoid writing into two system in the API call altogether.</p>
<img alt="An example flow of an ecommerce API, using the outbox pattern" loading="lazy" width="1500" height="500" decoding="async" data-nimg="1" style="color:transparent" srcset="/_next/image?url=%2Fexample-ecommerce-service-with-outbox.png&amp;w=1920&amp;q=75 1x, /_next/image?url=%2Fexample-ecommerce-service-with-outbox.png&amp;w=3840&amp;q=75 2x" src="/_next/image?url=%2Fexample-ecommerce-service-with-outbox.png&amp;w=3840&amp;q=75">
<p>The transactional outbox pattern is a simple but effective way to guarantee that messages are only published if the database transaction commits. We achieve this by not publishing the message directly but rather writing it to a event table as part of your transaction. From there a message relay system - a background worker - is picking up the entry and publishing it to your messaging platform.</p>
<p>The API endpoint implementation looks like this:</p>
<pre class="language-python"><code class="language-python"><span class="token decorator annotation punctuation">@router<span class="token punctuation">.</span>post</span><span class="token punctuation">(</span><span class="token string">'/orders'</span><span class="token punctuation">)</span>
<span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">create_order</span><span class="token punctuation">(</span>order<span class="token punctuation">)</span><span class="token punctuation">:</span>
	<span class="token keyword">async</span> <span class="token keyword">with</span> get_database_tranaction<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">as</span> tx<span class="token punctuation">:</span>
		order_created <span class="token operator">=</span> <span class="token keyword">await</span> save_to_database<span class="token punctuation">(</span>order<span class="token punctuation">)</span>
		order_created_event <span class="token operator">=</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
		<span class="token keyword">await</span> publish_event_to_outbox<span class="token punctuation">(</span>order_created_event<span class="token punctuation">)</span>

	<span class="token keyword">return</span> order_created
</code></pre>
<p>which effectively translates to the following SQL statements:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">BEGIN</span><span class="token punctuation">;</span>

<span class="token keyword">INSERT</span> <span class="token keyword">INTO</span> orders <span class="token punctuation">(</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">)</span> <span class="token keyword">VALUES</span> <span class="token punctuation">(</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">INSERT</span> <span class="token keyword">INTO</span> event_outbox <span class="token punctuation">(</span>event_type<span class="token punctuation">,</span> payload<span class="token punctuation">)</span> <span class="token keyword">VALUES</span> <span class="token punctuation">(</span><span class="token string">'order_created'</span><span class="token punctuation">,</span> <span class="token string">'{...}'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">COMMIT</span><span class="token punctuation">;</span>
</code></pre>
<h2>Implementing the message relay</h2>
<p>The obvious next question is how do you implement the message relay. There are multiple options to chose from, each with their own advantages, so choose wisely for your current situation.</p>
<h3>Simple: cronjob polling</h3>
<p>A simple scheduled job looks up the table for outstanding events in a fixed interval. It is advised to limit (e.g. 50) how many events at maximum each iteration can be processed.</p>
<p>After publishing the messages, you can mark the database entries as processed. If you fail to publish a message, a future iteration will pick up the same records and retry it for you. But be aware and monitor how many records are picked up from the database and how many are successfully published. If you are always picking up more events than you are successfully publishing, you build a backlog or worst case have a fault event that blocks all events after it from being published.</p>
<p>This options shines through its simplicity to implement, and it most suitable to get started. It lacks in terms of throughput and lag as its throughput is constant depending on the interval and the number of limit of records, and the lag introduced depending on the polling interval.
Example: It polls every 30 seconds for up to 100 records, means your max throughput is 200 records per minute and your worst case lag is 30 seconds.</p>
<p>Since events might be retried it must also be pointed out that your message processing must be able to handle idempotency as messages would be delivered at least once.</p>
<h3>Advanced: Leveraging Change Data Capture (CDC)</h3>
<p>With CDC, we can hook into the database’s transaction log, e.g. PostgreSQLs Write Ahead Log (WAL) or MySQLs Binary Log, and stream changes in real time.</p>
<p>In PostgreSQL you can create a logical replication slot, by using the <a href="https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-REPLICATION-TABLE">pg_create_logical_replication_slot</a> command, leveraging the <a href="https://github.com/eulerto/wal2json">wal2json</a> plugin.</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">SELECT</span> pg_create_logical_replication_slot<span class="token punctuation">(</span><span class="token string">'outbox_replication'</span><span class="token punctuation">,</span> <span class="token string">'wal2json'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<p>With the select commands</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">SELECT</span> pg_logical_slot_peek_changes<span class="token punctuation">(</span><span class="token string">'outbox_replication'</span><span class="token punctuation">,</span> <span class="token boolean">NULL</span><span class="token punctuation">,</span> <span class="token boolean">NULL</span><span class="token punctuation">,</span> <span class="token string">'include-tables'</span><span class="token punctuation">,</span> <span class="token string">'event_outbox'</span><span class="token punctuation">,</span> <span class="token string">'actions'</span><span class="token punctuation">,</span> <span class="token string">'insert'</span><span class="token punctuation">)</span> <span class="token comment">// peek into changes</span>
<span class="token keyword">SELECT</span> pg_logical_slot_get_changes<span class="token punctuation">(</span><span class="token string">'outbox_replication'</span><span class="token punctuation">,</span> <span class="token boolean">NULL</span><span class="token punctuation">,</span> <span class="token boolean">NULL</span><span class="token punctuation">,</span> <span class="token string">'include-tables'</span><span class="token punctuation">,</span> <span class="token string">'event_outbox'</span><span class="token punctuation">,</span> <span class="token string">'actions'</span><span class="token punctuation">,</span> <span class="token string">'insert'</span><span class="token punctuation">)</span> <span class="token comment">// consume changes</span>
</code></pre>
<p>we can peek and check for new entries and then consume them with <code>pg_logical_slot_get_changes</code>.</p>
<p>With this we can replace our scheduled poller into a more dynamic solution that has an improved throughput as shown in this pseudo code:</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">run</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
	<span class="token keyword">while</span> not_interrupted<span class="token punctuation">:</span>
		has_events <span class="token operator">=</span> <span class="token keyword">await</span> peek_for_outbox_events<span class="token punctuation">(</span><span class="token punctuation">)</span>

		<span class="token keyword">if</span> <span class="token keyword">not</span> has_events<span class="token punctuation">:</span>
			asyncio<span class="token punctuation">.</span>sleep<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span>

		events <span class="token operator">=</span> <span class="token keyword">await</span> get_outbox_events<span class="token punctuation">(</span>max_events<span class="token operator">=</span><span class="token number">10</span><span class="token punctuation">)</span>

		<span class="token keyword">for</span> event <span class="token keyword">in</span> events<span class="token punctuation">:</span>
			<span class="token keyword">try</span><span class="token punctuation">:</span>
				<span class="token keyword">await</span> publish<span class="token punctuation">(</span>event<span class="token punctuation">)</span>
			<span class="token keyword">except</span><span class="token punctuation">:</span>
				<span class="token keyword">await</span> dlq<span class="token punctuation">(</span>event<span class="token punctuation">)</span>
</code></pre>
<p>This way we optimised our scheduled job, for a better throughput an less lag. But when changes are consumed from the replication slot, they are no longer available in the next run, so any failures have to be put into a dead-letter-queue to be retried at a later stage.</p>
<p>This option comes with an increased operationally complexity and requires the understanding of replications slots. To reduce complexity managed CDC solutions like AWS DMS, Google DataStream, Artie or the open source variant Debezium can be used.</p>
<p>I personally only tried out AWS DMS and can recommend it, but it is advised to keep an eye on the DMS monitoring of how many records are processed, to ensure everything works as expected. An even better approach would be to monitor inserted records and CDC processed records as metrics as diverging values will directly make you aware of some issues in the system.</p>
<h2>Conclusion</h2>
<p>The Transaction outbox pattern elegantly sidesteps the dual write problem by ensuring atomicity between state changes and event emission. While it's not a silver bullet, it's a reliable and well-understood solution to one of the fundamental problems in distributed systems.</p>
<p>If you're building systems that need to be both responsive <em>and</em> reliable, this pattern should be a foundational part of your architecture.</p>
<p>Happy building and remember, build your system for failure!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Weekend build: Free local first image conversion in the browser]]></title>
            <link>https://dnnsthnnr.com/blog/weekend-build-free-local-first-image-conversion-in-the-browser</link>
            <guid>https://dnnsthnnr.com/blog/weekend-build-free-local-first-image-conversion-in-the-browser</guid>
            <pubDate>Sat, 07 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>While working on makemap.co I wanted to update some pictures and had to convert some images to webp format. Since it was only a handful of images, I did it manually using one of the first search results that popped up on Google.</p>
<p>Only afterward did I ask myself: did I just upload an image to a server? I went back to check what happens on the network tab in the developer console when converting an image. And to my surprise (and also not), the image was indeed uploaded.</p>
<p>Almost all results on the first pages of search results uploaded files to their servers for processing. This raised privacy concerns for me, especially when dealing with personal or sensitive images.</p>
<h2>The WebAssembly Revelation</h2>
<p>I wondered with the recent WebAssembly advancements if it must be possible to do almost all file conversions locally in the browser. I was almost certain that file manipulation tools have already been compiled or ported to WebAssembly.</p>
<p>A quick search later, I found that both libvips (powering Sharp, which powers Next.js image optimization) and ImageMagick already have WebAssembly versions. This was exciting news!</p>
<p>With this discovery, I came to the conclusion it would make for a great small weekend build project and got working. The result you can see at <a href="https://convertlocal.app">convertlocal.app</a> for yourself.</p>
<h2>Building a Local-First Conversion Tool</h2>
<p>The core principle behind convertlocal.app is simple: all processing happens directly in your browser. When you upload an image, the file is read into memory in your browser, a Web Worker is started to perform the file conversion using the WebAssembly module
and the result is saved back to your device.</p>
<h2>Current Features</h2>
<p>The current version of convertlocal.app supports common image conversion formats:</p>
<ul>
<li>Convert to WebP, PNG, JPEG, and other common formats</li>
<li>Adjust quality settings for lossy formats</li>
<li>Preserve metadata options</li>
<li>Batch processing of multiple files</li>
</ul>
<p>The tool is completely free to use and with no ads. It's a simple utility that does one thing well - convert your images locally.</p>
<h2>Long-Term Plans</h2>
<p>While the current version focuses on common image conversions, I have several plans for the future:</p>
<ul>
<li>Support for HEIC (High Efficiency Image Format) commonly used by iPhones</li>
<li>Expand beyond images to support other file types like documents and audio</li>
<li>Build a Raycast plugin that utilizes the same WebAssembly modules for file conversion directly from your desktop</li>
</ul>
<h2>Why This Matters</h2>
<p>In an era where data privacy is increasingly important, tools that process data locally rather than sending it to unknown servers provide a valuable alternative. While there are legitimate use cases for server-side processing (like when you need more computing power than the client device can provide), many common tasks can now be handled locally thanks to technologies like WebAssembly.</p>
<p>This small weekend project demonstrates how modern web technologies can be leveraged to create useful tools that respect user privacy while still providing powerful functionality.</p>
<p>If you need to convert images, give <a href="https://convertlocal.app">convertlocal.app</a> a try, and feel free to share feedback or feature requests!</p>]]></content:encoded>
        </item>
    </channel>
</rss>