<?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>Forem</title>
    <description>The most recent home feed on Forem.</description>
    <link>https://forem.com</link>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed"/>
    <language>en</language>
    <item>
      <title>Stop Using ngrok for Webhook Testing (A Simpler Way)</title>
      <dc:creator>Mahesh Srinivasan</dc:creator>
      <pubDate>Sun, 19 Apr 2026 09:19:48 +0000</pubDate>
      <link>https://forem.com/mahesh-dev/stop-using-ngrok-for-webhook-testing-a-simpler-way-37ak</link>
      <guid>https://forem.com/mahesh-dev/stop-using-ngrok-for-webhook-testing-a-simpler-way-37ak</guid>
      <description>&lt;p&gt;Debugging webhooks has always been… annoying.&lt;/p&gt;

&lt;p&gt;If you’ve worked with Stripe, GitHub, or any webhook-based system, you probably know this flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start your local server
&lt;/li&gt;
&lt;li&gt;Run ngrok
&lt;/li&gt;
&lt;li&gt;Copy the public URL
&lt;/li&gt;
&lt;li&gt;Paste it into your provider
&lt;/li&gt;
&lt;li&gt;Restart ngrok → URL changes
&lt;/li&gt;
&lt;li&gt;Repeat everything again
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It works… but it’s not smooth.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚨 The Problem
&lt;/h2&gt;

&lt;p&gt;Webhook testing shouldn’t feel like setup work.&lt;/p&gt;

&lt;p&gt;But right now, it often involves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Managing tunnels
&lt;/li&gt;
&lt;li&gt;Dealing with changing URLs
&lt;/li&gt;
&lt;li&gt;Debugging blindly when something fails
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And when you're just trying to test a simple webhook, this overhead slows you down.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚡ What I Wanted Instead
&lt;/h2&gt;

&lt;p&gt;I wanted something simpler:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run one command
&lt;/li&gt;
&lt;li&gt;Get a webhook URL instantly
&lt;/li&gt;
&lt;li&gt;Send requests
&lt;/li&gt;
&lt;li&gt;See everything in real-time
&lt;/li&gt;
&lt;li&gt;Forward directly to my local server
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No setup. No accounts. No friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔧 So I Built Anonymily
&lt;/h2&gt;

&lt;p&gt;A lightweight tool to debug webhooks locally — without ngrok or complex setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 How It Works
&lt;/h2&gt;

&lt;p&gt;Start your local server (for example on port 3000), then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;anonymily listen 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll instantly get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Forwarding to http://localhost:3000
Webhook URL: https://api.anonymily.com/h/abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now just send your webhook to that URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚡ What Happens Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Requests hit the public endpoint
&lt;/li&gt;
&lt;li&gt;They are streamed in real-time
&lt;/li&gt;
&lt;li&gt;Automatically forwarded to your localhost
&lt;/li&gt;
&lt;li&gt;You see the response instantly
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /webhook 200 OK (120ms)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No refreshing. No guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔌 Works With Anything
&lt;/h2&gt;

&lt;p&gt;You can use it with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stripe webhooks
&lt;/li&gt;
&lt;li&gt;GitHub events
&lt;/li&gt;
&lt;li&gt;Shopify hooks
&lt;/li&gt;
&lt;li&gt;Any custom webhook system
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it sends HTTP requests, it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔥 Why This Is Simpler
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No tunnel setup
&lt;/li&gt;
&lt;li&gt;No config
&lt;/li&gt;
&lt;li&gt;No login required
&lt;/li&gt;
&lt;li&gt;Instant endpoints
&lt;/li&gt;
&lt;li&gt;Real-time inspection
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just run → test → debug.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧪 Built for Fast Iteration
&lt;/h2&gt;

&lt;p&gt;When you're developing locally, speed matters.&lt;/p&gt;

&lt;p&gt;This tool is designed to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduce setup time
&lt;/li&gt;
&lt;li&gt;Remove friction
&lt;/li&gt;
&lt;li&gt;Keep you in flow
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So you can focus on debugging — not tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧹 Ephemeral by Default
&lt;/h2&gt;

&lt;p&gt;All webhook data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stored in memory
&lt;/li&gt;
&lt;li&gt;Automatically deleted after 24 hours
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No cleanup needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 Try It Out
&lt;/h2&gt;

&lt;p&gt;If you’re tired of setting up ngrok just to test webhooks, give this a try:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://anonymily.com" rel="noopener noreferrer"&gt;https://anonymily.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  💬 Feedback Welcome
&lt;/h2&gt;

&lt;p&gt;This is an early version, and I’d love to hear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What works well
&lt;/li&gt;
&lt;li&gt;What’s confusing
&lt;/li&gt;
&lt;li&gt;What you wish it had
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop your thoughts below 👇&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Select Properties with Select-Object: Show Only What Matters</title>
      <dc:creator>arnostorg</dc:creator>
      <pubDate>Sun, 19 Apr 2026 09:14:09 +0000</pubDate>
      <link>https://forem.com/arnostorg/select-properties-with-select-object-show-only-what-matters-46bi</link>
      <guid>https://forem.com/arnostorg/select-properties-with-select-object-show-only-what-matters-46bi</guid>
      <description>&lt;h1&gt;
  
  
  Select Properties with Select-Object: Show Only What Matters
&lt;/h1&gt;

&lt;p&gt;Too much information clutters your screen. Select-Object shows only the columns you care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;Select-Object picks specific properties from results and hides the rest. Imagine a spreadsheet with 50 columns—Select-Object lets you show only the 3 columns you actually need.&lt;/p&gt;

&lt;p&gt;This makes output cleaner and easier to read. It also helps you understand what data is available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Show Specific Columns
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Show only Name and Size (hides other properties)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Length&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Output:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Name           Length&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# ----           ------&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# report.txt     4521&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# document.xlsx  12048&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rename Columns While Showing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Show Length as 'Size in bytes'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"SizeInBytes"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;Expression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Output shows 'SizeInBytes' instead of 'Length'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Much clearer for other people reading your script!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Show First N Items
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Show only the first 5 files&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-First&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;5&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Show the last 10&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Last&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Combine with Sorting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Show name and size, sorted by size (biggest first)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sort-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Descending&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Result: Cleanest output showing biggest files first&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Most Used Options
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name, Length&lt;/strong&gt; - Show these specific properties&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;-First 5&lt;/strong&gt; - Show only first 5 items&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;-Last 10&lt;/strong&gt; - Show only last 10 items&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;@{Name='NewName';Expression={...}}&lt;/strong&gt; - Rename a property while displaying&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Trick: Power Usage
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Create readable process reports:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Show process names and memory, sorted by memory usage&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"MemoryMB"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;Expression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Memory&lt;/span&gt;&lt;span class="n"&gt;/1MB&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Sort&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MemoryMB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;Descending&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Shows processes from most to least memory-hungry&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Much easier to read than raw data!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pipeline cleanup pattern:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-gt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;MB&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sort-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Descending&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# 1. Get all files&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# 2. Filter to only big ones&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# 3. Show only name and size&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# 4. Sort by size&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Learn It Through Practice
&lt;/h2&gt;

&lt;p&gt;Stop reading and start practicing right now:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://windows-cli.arnost.org/en/powershell" rel="noopener noreferrer"&gt;Practice on your browser&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The interactive environment lets you type these commands and see real results immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next in PowerShell for Beginners
&lt;/h2&gt;

&lt;p&gt;This is part of the &lt;strong&gt;PowerShell for Beginners&lt;/strong&gt; series:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://windows-cli.arnost.org/en/powershell" rel="noopener noreferrer"&gt;Getting Started&lt;/a&gt; - Your first commands&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://windows-cli.arnost.org/en/powershell" rel="noopener noreferrer"&gt;Command Discovery&lt;/a&gt; - Find what exists&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://windows-cli.arnost.org/en/powershell" rel="noopener noreferrer"&gt;Getting Help&lt;/a&gt; - Understand commands&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://windows-cli.arnost.org/en/powershell" rel="noopener noreferrer"&gt;Working with Files&lt;/a&gt; - Copy, move, delete&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://windows-cli.arnost.org/en/powershell" rel="noopener noreferrer"&gt;Filtering Data&lt;/a&gt; - Where-Object and Select-Object&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://windows-cli.arnost.org/en/powershell" rel="noopener noreferrer"&gt;Pipelines&lt;/a&gt; - Chain commands together&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Related Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/powershell/" rel="noopener noreferrer"&gt;Official PowerShell Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/training/modules/introduction-to-powershell/" rel="noopener noreferrer"&gt;Microsoft Learn - PowerShell for IT Pros&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;You now understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How this command works&lt;/li&gt;
&lt;li&gt;The most common ways to use it&lt;/li&gt;
&lt;li&gt;One powerful trick to level up&lt;/li&gt;
&lt;li&gt;Where to practice hands-on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Practice these examples until they feel natural. Then tackle the next command in the series.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Ready to practice?&lt;/strong&gt; Head to the interactive environment and try these commands yourself. That's how it sticks!&lt;/p&gt;

&lt;p&gt;What PowerShell commands confuse you? Drop it in the comments!&lt;/p&gt;

</description>
      <category>powershell</category>
      <category>windows</category>
      <category>scripting</category>
      <category>beginners</category>
    </item>
    <item>
      <title>ChatGPT for Product Managers: Prompts That Sharpen Every Stage of the Product Cycle</title>
      <dc:creator>Tosh</dc:creator>
      <pubDate>Sun, 19 Apr 2026 09:12:19 +0000</pubDate>
      <link>https://forem.com/tosh2308/chatgpt-for-product-managers-prompts-that-sharpen-every-stage-of-the-product-cycle-226o</link>
      <guid>https://forem.com/tosh2308/chatgpt-for-product-managers-prompts-that-sharpen-every-stage-of-the-product-cycle-226o</guid>
      <description>&lt;h1&gt;
  
  
  ChatGPT for Product Managers: Prompts That Sharpen Every Stage of the Product Cycle
&lt;/h1&gt;

&lt;p&gt;I've been a product manager for seven years. I've survived brutal planning cycles, late-night stakeholder firefights, and sprint retros where nobody wanted to talk. What changed everything wasn't a new framework or a better roadmap tool — it was learning to use ChatGPT as a thinking partner at every stage of the product cycle.&lt;/p&gt;

&lt;p&gt;Here's what that actually looks like in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  PRD Drafting in Half the Time
&lt;/h2&gt;

&lt;p&gt;The blank PRD is one of the most demoralizing things in product. I used to spend two hours getting the structure right before I'd written a single meaningful sentence. Now I start with this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt; "You are a senior product manager. Draft a PRD outline for a [feature name] that solves [user problem] for [target user]. Include: problem statement, goals, non-goals, user stories, success metrics, and open questions. Be specific and opinionated."&lt;/p&gt;

&lt;p&gt;The output isn't final — it's a scaffold. I spend 20 minutes editing instead of 2 hours starting from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  User Story Generation That Actually Covers Edge Cases
&lt;/h2&gt;

&lt;p&gt;The hardest part of user stories isn't the happy path. It's the edge cases your engineers will discover at 11pm before launch. This prompt surfaces them early:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt; "Generate user stories for [feature] from the perspective of three different user types: a power user, a new user, and an edge-case user with accessibility needs. For each story, include the 'so that' rationale and one acceptance criterion."&lt;/p&gt;

&lt;p&gt;I paste these directly into Jira with light editing. My engineers stopped asking "but what about when..." during sprint planning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Competitive Analysis Without the Slide Deck Death March
&lt;/h2&gt;

&lt;p&gt;I used to spend a full day building competitive matrices. Now I use ChatGPT to accelerate the research synthesis:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt; "I'm building a feature comparison between [Product A], [Product B], and my product [Product C] across these dimensions: [list 4-5 dimensions]. Based on publicly known information, describe the strengths and weaknesses of each. Flag where you're uncertain."&lt;/p&gt;

&lt;p&gt;Pair this with your own research and you have a first draft in 30 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sprint Retro Summaries That People Actually Read
&lt;/h2&gt;

&lt;p&gt;Most retro notes are a list of complaints nobody revisits. I now use ChatGPT to turn raw retro output into something actionable:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt; "Here are the raw notes from our sprint retro: [paste notes]. Summarize into: 3 wins, 3 recurring friction points, and 2 concrete action items the team agreed on. Keep each point to one sentence."&lt;/p&gt;

&lt;p&gt;I share this in Slack before the retro ends. Engagement tripled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stakeholder Update Emails That Get Read
&lt;/h2&gt;

&lt;p&gt;The update email is a tax on PMs. Everyone wants them; nobody wants to write them. I now generate a first draft in 60 seconds:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt; "Write a stakeholder update email for a product team. Context: [brief status]. Audience: [executive or cross-functional team]. Tone: direct and confident. Include: what shipped, what's next, and one risk to flag. Keep it under 150 words."&lt;/p&gt;

&lt;p&gt;I edit for specifics, but the hard part — structure and tone — is handled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt Stack for the Full Cycle
&lt;/h2&gt;

&lt;p&gt;Beyond the five prompts above, here are three more I reach for constantly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt; "I need to prioritize these 6 features using the RICE framework. Here's what I know about each: [paste feature list with rough data]. Help me estimate reach, impact, confidence, and effort for each, and flag your assumptions."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt; "Write interview questions for a user research session about [topic]. Target user: [persona]. Goal: understand their current workflow and pain points. Avoid leading questions."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt; "Review this product spec for logical gaps, missing edge cases, or unstated assumptions. [Paste spec]. Be direct about what's unclear."&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Saves Real Time
&lt;/h2&gt;

&lt;p&gt;In a normal week, these prompts save me three to four hours. That's not theoretical — it's time I've reclaimed from low-value drafting and redirected into customer calls and engineering conversations. The prompts don't replace judgment. They eliminate the friction that delays it.&lt;/p&gt;

&lt;p&gt;If you want a curated set of 50 prompts built specifically for product managers — covering roadmap communication, stakeholder management, discovery interviews, and more — I put them all in one place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://toshleonard.gumroad.com/l/rzenot" rel="noopener noreferrer"&gt;Get the ChatGPT Prompt Pack for Professionals — $27&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's the shortcut I wish I'd had in year one.&lt;/p&gt;

</description>
      <category>chatgpt</category>
      <category>productivity</category>
      <category>ai</category>
    </item>
    <item>
      <title>Handy Agent 2026 – From Chatbot to Autonomous AI Systems</title>
      <dc:creator>ITPrep</dc:creator>
      <pubDate>Sun, 19 Apr 2026 09:08:53 +0000</pubDate>
      <link>https://forem.com/itprepvn/handy-agent-2026-from-chatbot-to-autonomous-ai-systems-ejh</link>
      <guid>https://forem.com/itprepvn/handy-agent-2026-from-chatbot-to-autonomous-ai-systems-ejh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;"AI is no longer just responding to prompts.&lt;br&gt;
It is executing workflows."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The last few years gave us powerful models like ChatGPT, Gemini, and Claude.&lt;/p&gt;

&lt;p&gt;But let’s be real.&lt;/p&gt;

&lt;p&gt;Most people are still stuck in this loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prompt → Copy → Paste → Repeat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not automation.&lt;br&gt;
That is manual work with extra steps.&lt;/p&gt;


&lt;h2&gt;
  
  
  ⚡ The Paradigm Shift
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Old World&lt;/th&gt;
&lt;th&gt;New World&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chatbot&lt;/td&gt;
&lt;td&gt;Handy Agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answering&lt;/td&gt;
&lt;td&gt;Executing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Passive&lt;/td&gt;
&lt;td&gt;Autonomous&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We are moving from &lt;strong&gt;Conversational AI → Action-oriented AI&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🧠 What is a Handy Agent?
&lt;/h2&gt;

&lt;p&gt;A Handy Agent is an AI system that can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand goals&lt;/li&gt;
&lt;li&gt;Break tasks into steps&lt;/li&gt;
&lt;li&gt;Use tools (APIs, apps)&lt;/li&gt;
&lt;li&gt;Execute actions automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In one sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;It bridges the gap between intention and execution.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  🔄 How Handy Agents Think (ReAct Loop)
&lt;/h2&gt;

&lt;p&gt;At the core is a reasoning loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Thought → Action → Observation → Repeat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"thought"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User wants to schedule meeting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"check_calendar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"next_step"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"find_available_slot"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This loop continues until the task is completed.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧩 System Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. LLM (The Brain)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;GPT / Claude / Gemini&lt;/li&gt;
&lt;li&gt;Handles reasoning &amp;amp; planning&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Tool Layer (Execution)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;APIs (Google, Slack, CRM)&lt;/li&gt;
&lt;li&gt;Executes real-world actions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Memory Layer
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Short-term: task context&lt;/li&gt;
&lt;li&gt;Long-term: vector DB (RAG)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ User Goal ]
      ↓
[ LLM Reasoning ]
      ↓
[ Tool Execution ]
      ↓
[ Memory Update ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ⚔️ Chatbot vs Handy Agent (Real Scenario)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Chatbot
&lt;/h3&gt;

&lt;p&gt;You:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Schedule a meeting"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"3PM works well"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You still:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open calendar&lt;/li&gt;
&lt;li&gt;Create event&lt;/li&gt;
&lt;li&gt;Send invites&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Handy Agent
&lt;/h3&gt;

&lt;p&gt;You:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Schedule a meeting with marketing team"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checks availability&lt;/li&gt;
&lt;li&gt;Picks optimal time&lt;/li&gt;
&lt;li&gt;Creates event&lt;/li&gt;
&lt;li&gt;Sends invites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Time: &lt;strong&gt;10 minutes → 10 seconds&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🔥 Real-World Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Administrative Automation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Meeting scheduling&lt;/li&gt;
&lt;li&gt;Email handling&lt;/li&gt;
&lt;li&gt;Calendar management&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Data Pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CRM → Data Cleaning → Analysis → PDF Report → Email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fully automated.&lt;/p&gt;




&lt;h3&gt;
  
  
  Content Engine
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Generate posts&lt;/li&gt;
&lt;li&gt;Create images&lt;/li&gt;
&lt;li&gt;Schedule publishing&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🛠️ Build Your First Handy Agent (No Code)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Define Scope
&lt;/h3&gt;

&lt;p&gt;Start small:&lt;/p&gt;

&lt;p&gt;"Auto extract invoice data from email"&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2: Choose Platform
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Zapier&lt;/li&gt;
&lt;li&gt;Make.com&lt;/li&gt;
&lt;li&gt;Flowise&lt;/li&gt;
&lt;li&gt;Dify&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Step 3: Connect Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Gmail&lt;/li&gt;
&lt;li&gt;Google Sheets&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Step 4: Define Logic
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IF email contains "invoice"
THEN extract data
AND save to sheet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🌍 The Future: Agent-to-Agent Economy
&lt;/h2&gt;

&lt;p&gt;Soon, agents will interact with other agents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Agent ↔ Vendor Agent ↔ Payment System
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decisions and transactions will happen automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚠️ Risks &amp;amp; Challenges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Hallucination → incorrect actions&lt;/li&gt;
&lt;li&gt;Security → prompt injection risks&lt;/li&gt;
&lt;li&gt;Cost → token usage can scale quickly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Solution: &lt;strong&gt;Human-in-the-loop control&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 Deep Dive &amp;amp; Resources
&lt;/h2&gt;

&lt;p&gt;If you want a full, in-depth breakdown of Handy Agents (architecture, real workflows, and business impact), check out the original article:&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://itprep.com.vn/gioi-thieu-ve-handy-agent-chuyen-sau-ve-ai-thuc-thi/" rel="noopener noreferrer"&gt;https://itprep.com.vn/gioi-thieu-ve-handy-agent-chuyen-sau-ve-ai-thuc-thi/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For more AI, backend, and developer-focused content, visit:&lt;/p&gt;

&lt;p&gt;🏠 &lt;a href="https://itprep.com.vn/" rel="noopener noreferrer"&gt;https://itprep.com.vn/&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Final Thought
&lt;/h2&gt;

&lt;p&gt;Using AI only for answers is like using a supercomputer as a search engine.&lt;/p&gt;

&lt;p&gt;The real value comes from execution.&lt;/p&gt;




&lt;p&gt;💬 What would your first Handy Agent automate?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>futurechallenge</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Modular Monolith 2026 Complete Guide — Spring Modulith, ArchUnit Fitness Functions, and Lessons from Shopify's 30TB/min Architecture</title>
      <dc:creator>daniel jeong</dc:creator>
      <pubDate>Sun, 19 Apr 2026 09:05:42 +0000</pubDate>
      <link>https://forem.com/x4nent/the-modular-monolith-2026-complete-guide-spring-modulith-archunit-fitness-functions-and-lessons-878</link>
      <guid>https://forem.com/x4nent/the-modular-monolith-2026-complete-guide-spring-modulith-archunit-fitness-functions-and-lessons-878</guid>
      <description>&lt;p&gt;The biggest architectural inflection point of 2026 is the "microservices regression" phenomenon. According to the &lt;strong&gt;CNCF Q1 2026 report&lt;/strong&gt;, &lt;strong&gt;42% of organizations that initially adopted microservices have consolidated some services into larger deployable units&lt;/strong&gt; — and that consolidated form is the &lt;strong&gt;Modular Monolith&lt;/strong&gt;. This architecture keeps the operational simplicity of a single deployable unit while explicitly enforcing domain boundaries, and it recently drew attention after Shopify processed a peak of &lt;strong&gt;30TB/minute&lt;/strong&gt; during Black Friday 2025 without incident. In Q1 2026, &lt;strong&gt;Spring Modulith 1.4 GA&lt;/strong&gt;, &lt;strong&gt;ArchUnit 1.3&lt;/strong&gt;, and &lt;strong&gt;jMolecules 2026.0&lt;/strong&gt; all landed, signaling the maturity of the &lt;strong&gt;Evolutionary Architecture&lt;/strong&gt; toolchain. Neal Ford and Sam Newman went as far as declaring 2026 "the renaissance of the monolith." This guide walks through adoption, operation, and evolution of Modular Monoliths from a ManoIT production perspective, with benchmarks and real code.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Why Modular Monolith in 2026 — The Structural Causes of Microservices Fatigue
&lt;/h2&gt;

&lt;p&gt;From the late 2010s into the early 2020s, the industry treated "microservices" as synonymous with "modern architecture." That consensus collapsed between 2024 and 2026. &lt;strong&gt;Amazon Prime Video&lt;/strong&gt; rolled its video quality monitoring service from microservices back to a single monolith and reported a &lt;strong&gt;90% cost reduction&lt;/strong&gt;. &lt;strong&gt;Segment, InVision, and Istio&lt;/strong&gt; announced similar regressions. In early 2026, a joint CNCF/SlashData survey found that "satisfaction with microservices architecture" had dropped &lt;strong&gt;19pp&lt;/strong&gt; compared to 2024.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;2024&lt;/th&gt;
&lt;th&gt;2026 Q1&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Orgs that consolidated after initial microservices adoption&lt;/td&gt;
&lt;td&gt;23%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;42%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+19pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agree: "Microservices are overkill for teams of 10 or fewer"&lt;/td&gt;
&lt;td&gt;61%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;84%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+23pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Share of new 2026 projects adopting modular monolith&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;37%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+23pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Modulith GitHub stars (1-year growth)&lt;/td&gt;
&lt;td&gt;2.4k&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.1k&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+278%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArchUnit monthly downloads on Maven Central&lt;/td&gt;
&lt;td&gt;3.2M&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.8M&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+175%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The problems microservices solved — independent team deployments, language heterogeneity, fault isolation — remain valid. But evidence piled up that distributed transactions, the cognitive load of eventual consistency, network latency, tracing complexity, Kubernetes operational cost, and CI/CD fragmentation erode early-team productivity. &lt;strong&gt;Neal Ford&lt;/strong&gt; summarized it in his 2026 QCon keynote: "Microservices are not a destination, they are a path — and most projects don't need more than half of it."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Important: A modular monolith is &lt;strong&gt;not a retreat from microservices&lt;/strong&gt;. It is an "evolutionary middle ground" that uses static analysis and runtime events to enforce domain boundaries, while making it &lt;strong&gt;cheap to extract a module into a microservice when actually needed&lt;/strong&gt;. Finding the right boundaries is the hardest problem in distributed systems, and the modular monolith provides a low-cost environment for exploring and adjusting those boundaries.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  2. Five Structural Principles of a Modular Monolith
&lt;/h2&gt;

&lt;p&gt;Simon Brown coined the term in 2015 in "Monoliths vs Microservices is Missing the Point." Sam Newman's 2026 second edition of &lt;em&gt;Monolith to Microservices&lt;/em&gt; re-anchored it for the modern era. The core idea is &lt;strong&gt;explicit, enforced module boundaries within a single deployable unit&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Principle&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Tooling&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Explicit module boundaries&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Packages/namespaces separate modules; each is treated as an independently deployable unit&lt;/td&gt;
&lt;td&gt;Spring Modulith, Maven multi-module, Gradle subprojects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unidirectional dependencies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No cyclic dependencies; modules interact only through published APIs (Ports)&lt;/td&gt;
&lt;td&gt;ArchUnit, jMolecules, deptrac&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hidden internal state&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Internal classes/tables/entities are off-limits to other modules; &lt;code&gt;internal&lt;/code&gt; package convention&lt;/td&gt;
&lt;td&gt;JPMS, Kotlin &lt;code&gt;internal&lt;/code&gt;, TypeScript &lt;code&gt;package.json exports&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Event-first communication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Domain events over synchronous calls to keep coupling loose&lt;/td&gt;
&lt;td&gt;Spring ApplicationEvents, MediatR, Transactional Outbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Architecture fitness functions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CI automatically verifies boundary and dependency rules; builds fail on violations&lt;/td&gt;
&lt;td&gt;ArchUnit, pytestarch, custom Checkstyle rules&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key idea here is &lt;strong&gt;architecture fitness functions&lt;/strong&gt;, introduced by Neal Ford and Rebecca Parsons in &lt;em&gt;Building Evolutionary Architectures&lt;/em&gt;. Fitness functions &lt;strong&gt;express architectural characteristics as executable tests&lt;/strong&gt;. Just as unit tests guard business logic, fitness functions guard architectural integrity.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Spring Modulith 1.4 GA — The 2026 Reference Implementation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Spring Modulith 1.4&lt;/strong&gt; went GA on March 27, 2026. Built on Spring Boot 3.5 and Java 21, it adds &lt;code&gt;@ApplicationModule&lt;/code&gt;, Event Externalization, a documentation generator, and observability integration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// package-info.java — placed at the module root package&lt;/span&gt;
&lt;span class="nd"&gt;@org&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;springframework&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;modulith&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ApplicationModule&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;displayName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Order Management"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;allowedDependencies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"common"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"catalog::api"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// only catalog's public API is reachable&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.manoit.lms.order&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// OrderService.java — emit domain events for loose coupling&lt;/span&gt;
&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.manoit.lms.order&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.modulith.events.ApplicationModuleListener&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.context.ApplicationEventPublisher&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="nd"&gt;@RequiredArgsConstructor&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ApplicationEventPublisher&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Transactional&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="nf"&gt;placeOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderCommand&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Prefer events over direct calls — catalog/inventory modules react as subscribers&lt;/span&gt;
        &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publishEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderPlacedEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// InventoryEventHandler.java — listener in a different module&lt;/span&gt;
&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.manoit.lms.inventory&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InventoryEventHandler&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@ApplicationModuleListener&lt;/span&gt;  &lt;span class="c1"&gt;// Splits the tx boundary, auto-handles retries and DLQ&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderPlacedEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Deduct inventory (async, independent transaction)&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@ApplicationModuleListener&lt;/code&gt; bundles &lt;code&gt;@TransactionalEventListener(AFTER_COMMIT) + @Async + Transactional Outbox&lt;/code&gt; behind a single annotation. The decisive improvements in Spring Modulith 1.4 are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;1.3&lt;/th&gt;
&lt;th&gt;1.4&lt;/th&gt;
&lt;th&gt;Production impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Event Externalization (Kafka/RabbitMQ/JMS)&lt;/td&gt;
&lt;td&gt;Experimental&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;GA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Internal events auto-published to external brokers — an escape hatch to microservices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observability (Micrometer/OpenTelemetry)&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Auto-instrumented&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Inter-module calls and events appear as spans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-generated module docs (C4 model)&lt;/td&gt;
&lt;td&gt;Text only&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;PlantUML + Structurizr&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Architecture diagrams become a build artifact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration test helpers&lt;/td&gt;
&lt;td&gt;Module-scoped&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Scenario DSL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Test event chains with &lt;code&gt;Scenario.publish().andWaitFor()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Named interfaces&lt;/td&gt;
&lt;td&gt;Single &lt;code&gt;api&lt;/code&gt; package&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Multiple interfaces&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;catalog::admin-api&lt;/code&gt;, &lt;code&gt;catalog::public-api&lt;/code&gt;, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Event Externalization going GA is the strategic highlight. When a single module eventually must be extracted to a microservice, having events already flowing through an external broker drops the extraction cost by &lt;strong&gt;more than 70%&lt;/strong&gt;. The evolutionary path is baked into the architecture itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Architecture Fitness Functions with ArchUnit 1.3
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ArchUnit 1.3&lt;/strong&gt; (February 2026) has full JUnit 5, Kotlin, record-type, pattern-matching, and DSL support, and its new &lt;code&gt;FreezingArchRule&lt;/code&gt; makes incremental adoption in legacy codebases feasible. The following is the ManoIT standard rule set.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ArchitectureRulesTest.java — gate conditions on the CI pipeline&lt;/span&gt;
&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.manoit.lms.architecture&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.tngtech.archunit.junit.AnalyzeClasses&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.tngtech.archunit.junit.ArchTest&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.tngtech.archunit.lang.ArchRule&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tngtech&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;archunit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;syntax&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ArchRuleDefinition&lt;/span&gt;&lt;span class="o"&gt;.*;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tngtech&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;archunit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;library&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Architectures&lt;/span&gt;&lt;span class="o"&gt;.*;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tngtech&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;archunit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;library&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;modules&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ModuleRuleDefinition&lt;/span&gt;&lt;span class="o"&gt;.*;&lt;/span&gt;

&lt;span class="nd"&gt;@AnalyzeClasses&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;packages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.manoit.lms"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;importOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ImportOption&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DoNotIncludeTests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArchitectureRulesTest&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Rule 1: Respect hexagonal layering&lt;/span&gt;
    &lt;span class="nd"&gt;@ArchTest&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ArchRule&lt;/span&gt; &lt;span class="n"&gt;hexagonal_layers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;layeredArchitecture&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;consideringAllDependencies&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;layer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Domain"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;definedBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"..domain.."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;layer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Application"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;definedBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"..application.."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;layer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Adapter"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;definedBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"..adapter.."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;whereLayer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Adapter"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;mayNotBeAccessedByAnyLayer&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;whereLayer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Application"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;mayOnlyBeAccessedByLayers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Adapter"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;whereLayer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Domain"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;mayOnlyBeAccessedByLayers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Application"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Rule 2: Inter-module access only through api packages&lt;/span&gt;
    &lt;span class="nd"&gt;@ArchTest&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ArchRule&lt;/span&gt; &lt;span class="n"&gt;module_boundary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modules&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;definedByRootClasses&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;should&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;respectTheirAllowedDependencies&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Rule 3: No direct references to Entities from outside the repository package&lt;/span&gt;
    &lt;span class="nd"&gt;@ArchTest&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ArchRule&lt;/span&gt; &lt;span class="n"&gt;entity_encapsulation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;noClasses&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;that&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;resideOutsideOfPackages&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"..domain.."&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"..repository.."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;should&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;dependOnClassesThat&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;areAnnotatedWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Entity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Rule 4: Controllers call services only, never repositories directly&lt;/span&gt;
    &lt;span class="nd"&gt;@ArchTest&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ArchRule&lt;/span&gt; &lt;span class="n"&gt;controller_isolation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;noClasses&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;that&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;resideInAPackage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"..adapter.web.."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;should&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;dependOnClassesThat&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;resideInAPackage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"..repository.."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Rule 5: Zero circular dependencies&lt;/span&gt;
    &lt;span class="nd"&gt;@ArchTest&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ArchRule&lt;/span&gt; &lt;span class="n"&gt;no_cycles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;matching&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.manoit.lms.(*).."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;should&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;beFreeOfCycles&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These five rules run on every commit in the build pipeline. A violation &lt;strong&gt;blocks PR merges&lt;/strong&gt;, which structurally prevents "architectural drift." This is the same strategy deployed by Google, Shopify, and Netflix.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Shopify — 30TB/min on Black Friday, 2M Classes in a Modular Monolith
&lt;/h2&gt;

&lt;p&gt;Shopify runs the largest Ruby on Rails monolith in the world. Per their 2026 engineering blog, the monolith contains &lt;strong&gt;roughly 2M classes, more than 4,000 components, and hundreds of concurrent contributors&lt;/strong&gt;, and it handled a peak of &lt;strong&gt;30TB/minute&lt;/strong&gt; during Black Friday Cyber Monday (BFCM) 2025.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shopify platform metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Codebase size&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~4M lines&lt;/strong&gt; of Ruby + TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Components (modules)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4,000+&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily production deploys&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200+ per day&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BFCM 2025 peak throughput&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;30TB/min&lt;/strong&gt;, ~$4B in GMV&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture rule violation block rate in CI&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Shopify's secret is a proprietary static analysis tool called &lt;strong&gt;Packwerk&lt;/strong&gt;. Packwerk enforces explicit boundaries in a Ruby codebase via &lt;code&gt;package.yml&lt;/code&gt;, playing a role analogous to ArchUnit. The core design principles are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pack-level boundaries&lt;/strong&gt; — each pack splits into a &lt;code&gt;public/&lt;/code&gt; API and private internals; outside packs can only import from &lt;code&gt;public/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit dependency graph&lt;/strong&gt; — allowed packs are listed in a &lt;code&gt;dependencies&lt;/code&gt; YAML; CI fails on violations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain events first&lt;/strong&gt; — synchronous calls between packs are minimized, using Rails &lt;code&gt;ActiveSupport::Notifications&lt;/code&gt; for async propagation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checkpoint-managed refactoring&lt;/strong&gt; — violations are snapshotted into &lt;code&gt;deprecated_references.yml&lt;/code&gt; and then whittled down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Module ownership&lt;/strong&gt; — &lt;code&gt;CODEOWNERS&lt;/code&gt; plus pack metadata establishes team-level ownership — by domain, not file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Shopify's conclusion is sharp: "We did not need to move to microservices. What we needed was &lt;strong&gt;order inside the monolith&lt;/strong&gt;." Internal measurements published in April 2026 report that after adopting pack-based modularization, new-developer onboarding time fell by &lt;strong&gt;55%&lt;/strong&gt;, and cross-module regressions dropped by &lt;strong&gt;68% year-over-year&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Decision Framework — When Modular Monolith, When Microservices
&lt;/h2&gt;

&lt;p&gt;Based on Sam Newman's four-axis decision matrix in the 2026 second edition of &lt;em&gt;Monolith to Microservices&lt;/em&gt;, here are the criteria ManoIT applies when starting a new project.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;Favors Modular Monolith&lt;/th&gt;
&lt;th&gt;Favors Microservices&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Team size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1–20 people, single or a few teams&lt;/td&gt;
&lt;td&gt;50+ across many autonomous teams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Domain-boundary certainty&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Still being explored — refactoring cost must stay low&lt;/td&gt;
&lt;td&gt;Boundaries are clear and stable; aligned with Conway's Law&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment frequency/independence&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1–10 deploys/day, coordinated deploys acceptable&lt;/td&gt;
&lt;td&gt;Dozens per hour, fully independent team deploys required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scaling pattern&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Horizontal replication suffices, load distributed evenly&lt;/td&gt;
&lt;td&gt;Extreme per-module load variance, GPUs or special hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data consistency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ACID / strong consistency needed, avoids distributed tx complexity&lt;/td&gt;
&lt;td&gt;Eventual consistency acceptable, CDC/Saga available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Operational maturity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Kubernetes, service mesh, distributed tracing not yet mature&lt;/td&gt;
&lt;td&gt;Platform team in place, SRE established, full observability stack&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notably, Martin Fowler's &lt;strong&gt;"MonolithFirst"&lt;/strong&gt; principle is regaining traction in 2026. Fowler argued: "Start a new system as a monolith; once the boundaries stabilize, split out services." The 2026 reality is that most projects do not even need that split. &lt;strong&gt;More than 90% of enterprise projects are adequately served by a well-designed modular monolith.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Migration Path — The Reverse Strangler Fig Pattern
&lt;/h2&gt;

&lt;p&gt;For teams already sprawled across many microservices, Newman proposes a &lt;strong&gt;Reverse Strangler&lt;/strong&gt; pattern for re-consolidation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Step 1: Pick consolidation targets by measuring joint change frequency&lt;/span&gt;
&lt;span class="c1"&gt;# Analyze git history to find "services that change together"&lt;/span&gt;
&lt;span class="s"&gt;$ git log --name-only --since="6 months ago" | \&lt;/span&gt;
    &lt;span class="s"&gt;awk '/services\//' | \&lt;/span&gt;
    &lt;span class="s"&gt;sort | uniq -c | sort -rn | head -20&lt;/span&gt;
&lt;span class="c1"&gt;# Result: order + payment + inventory change together in 72% of commits → consolidate&lt;/span&gt;

&lt;span class="c1"&gt;# Step 2: Centralize routing at the API gateway&lt;/span&gt;
&lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/api/order/*&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monolith-v2&lt;/span&gt;  &lt;span class="c1"&gt;# new modular monolith&lt;/span&gt;
    &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;legacy-order-service&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/api/payment/*&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monolith-v2&lt;/span&gt;
    &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;legacy-payment-service&lt;/span&gt;

&lt;span class="c1"&gt;# Step 3: Absorb into Spring Modulith modules&lt;/span&gt;
&lt;span class="c1"&gt;# order service code  → /modules/order/&lt;/span&gt;
&lt;span class="c1"&gt;# payment service code → /modules/payment/&lt;/span&gt;
&lt;span class="c1"&gt;# Database: per-service schema → single DB + schema-per-module&lt;/span&gt;

&lt;span class="c1"&gt;# Step 4: Verify consolidation with ArchUnit fitness functions&lt;/span&gt;
&lt;span class="c1"&gt;# Zero circular dependencies, zero module boundary violations&lt;/span&gt;

&lt;span class="c1"&gt;# Step 5: Gradually shift traffic away from legacy services (10% → 50% → 100%)&lt;/span&gt;
&lt;span class="c1"&gt;# Canary deploys + error-rate SLO monitoring&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In one ManoIT customer engagement, 7 of 12 microservices were consolidated into a modular monolith, resulting in &lt;strong&gt;58% infrastructure cost savings, 42% reduction in median response latency, and 73% fewer incident pages&lt;/strong&gt;. The remaining 5 services — GPU inference, bulk batch jobs, and outbound email — had fundamentally different workload characteristics and were intentionally kept separate.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Modular Monoliths in Python/Django and Node.js
&lt;/h2&gt;

&lt;p&gt;Modular monoliths are not JVM-only. Here's a 2026 snapshot of modularization support across major frameworks.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language/Framework&lt;/th&gt;
&lt;th&gt;Modularization tooling&lt;/th&gt;
&lt;th&gt;Highlights&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Java/Spring&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Spring Modulith 1.4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@ApplicationModule&lt;/code&gt;, Event Externalization, ArchUnit integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kotlin&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Arrow + ArchUnit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;internal&lt;/code&gt; visibility, Gradle subprojects, sealed interfaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;pytestarch 3.0, import-linter 2.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Package dependency rules, FastAPI Router modularization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript/Node&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Nx 20, turbo 2, dependency-cruiser 16&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Monorepo workspaces, &lt;code&gt;exports&lt;/code&gt;-field boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ruby on Rails&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Packwerk 3, Engines&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pack-level boundaries, enforced &lt;code&gt;public/&lt;/code&gt; API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C#/.NET&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;NetArchTest 2, MediatR 12&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Assembly split, vertical slice architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;go-cleanarch, deptrac-go&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Package dependency tests, interface boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;FastAPI-based ManoIT projects rely on &lt;code&gt;import-linter&lt;/code&gt; to enforce module boundaries. Minimal configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# setup.cfg — import-linter configuration
&lt;/span&gt;&lt;span class="nn"&gt;[importlinter]&lt;/span&gt;
&lt;span class="py"&gt;root_package&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;

&lt;span class="nn"&gt;[importlinter:contract:layered_architecture]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Layered Architecture&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;layers&lt;/span&gt;
&lt;span class="py"&gt;layers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="err"&gt;app.adapter&lt;/span&gt;
    &lt;span class="err"&gt;app.application&lt;/span&gt;
    &lt;span class="err"&gt;app.domain&lt;/span&gt;

&lt;span class="nn"&gt;[importlinter:contract:module_boundary]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Module Boundary — order cannot import payment.internal&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;forbidden&lt;/span&gt;
&lt;span class="py"&gt;source_modules&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="err"&gt;app.modules.order&lt;/span&gt;
&lt;span class="py"&gt;forbidden_modules&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="err"&gt;app.modules.payment.internal&lt;/span&gt;
    &lt;span class="err"&gt;app.modules.inventory.internal&lt;/span&gt;

&lt;span class="nn"&gt;[importlinter:contract:no_cycles]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;No Circular Dependencies&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;independence&lt;/span&gt;
&lt;span class="py"&gt;modules&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="err"&gt;app.modules.order&lt;/span&gt;
    &lt;span class="err"&gt;app.modules.payment&lt;/span&gt;
    &lt;span class="err"&gt;app.modules.inventory&lt;/span&gt;
    &lt;span class="err"&gt;app.modules.catalog&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;poetry run lint-imports&lt;/code&gt; in CI instantly catches boundary violations and integrates with any GitHub Actions or GitLab CI pipeline in under a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Observability — Tracing Inter-Module Interactions
&lt;/h2&gt;

&lt;p&gt;Observability for a modular monolith requires a different approach than microservices. The &lt;strong&gt;OpenTelemetry 2026 semantic conventions&lt;/strong&gt; standardize &lt;code&gt;module.name&lt;/code&gt; and &lt;code&gt;module.interface&lt;/code&gt; attributes, and Spring Modulith 1.4 injects them automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// application.yml — enable Spring Modulith observability&lt;/span&gt;
&lt;span class="nl"&gt;spring:&lt;/span&gt;
  &lt;span class="nl"&gt;modulith:&lt;/span&gt;
    &lt;span class="nl"&gt;events:&lt;/span&gt;
      &lt;span class="nl"&gt;externalization:&lt;/span&gt;
        &lt;span class="nl"&gt;enabled:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="nl"&gt;broker:&lt;/span&gt; &lt;span class="n"&gt;kafka&lt;/span&gt;
    &lt;span class="nl"&gt;observability:&lt;/span&gt;
      &lt;span class="nl"&gt;enabled:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;instrument&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="n"&gt;boundaries&lt;/span&gt;
      &lt;span class="nl"&gt;tags:&lt;/span&gt;
        &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sla&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;critical&lt;/span&gt;

&lt;span class="nl"&gt;management:&lt;/span&gt;
  &lt;span class="nl"&gt;otlp:&lt;/span&gt;
    &lt;span class="nl"&gt;tracing:&lt;/span&gt;
      &lt;span class="nl"&gt;endpoint:&lt;/span&gt; &lt;span class="nl"&gt;http:&lt;/span&gt;&lt;span class="c1"&gt;//otel-collector:4318/v1/traces&lt;/span&gt;
    &lt;span class="nl"&gt;metrics:&lt;/span&gt;
      &lt;span class="nl"&gt;export:&lt;/span&gt;
        &lt;span class="nl"&gt;enabled:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nl"&gt;tracing:&lt;/span&gt;
    &lt;span class="nl"&gt;sampling:&lt;/span&gt;
      &lt;span class="nl"&gt;probability:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;  &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is that Grafana Tempo or Jaeger visualizes the flow &lt;strong&gt;"order module → payment module → inventory module"&lt;/strong&gt; as a distributed trace. Because it is all in-process, there is no network-hop cost — only the logical call chain is traced. This is extremely useful for debugging and for defining SLOs.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. ManoIT Production Checklist
&lt;/h2&gt;

&lt;p&gt;Standard checklist for teams considering a modular monolith:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Model the domain first.&lt;/strong&gt; Run Event Storming or Domain Storytelling workshops to identify bounded contexts. When boundaries are uncertain, start with coarser modules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stamp out a module skeleton.&lt;/strong&gt; Each module uses a fixed &lt;code&gt;api/&lt;/code&gt;, &lt;code&gt;application/&lt;/code&gt;, &lt;code&gt;domain/&lt;/code&gt;, &lt;code&gt;adapter/&lt;/code&gt; layout. Provide a template scaffold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add Spring Modulith 1.4 and ArchUnit 1.3 dependencies.&lt;/strong&gt; Drop them into Maven/Gradle and commit your first fitness functions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wire into CI.&lt;/strong&gt; Run architecture rules alongside unit tests via &lt;code&gt;mvn test&lt;/code&gt; or &lt;code&gt;gradle check&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer event-first communication.&lt;/strong&gt; If three or more synchronous inter-module calls chain together, redesign with events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up a Transactional Outbox.&lt;/strong&gt; Use Event Externalization GA to publish to Kafka/RabbitMQ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-generate C4 diagrams.&lt;/strong&gt; Add Spring Modulith's &lt;code&gt;Documenter&lt;/code&gt; to the build so PlantUML outputs ship with every build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instrument 100% of boundaries.&lt;/strong&gt; OpenTelemetry + Micrometer spans on every module boundary, 0.1 sampling in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-define scaling SLOs.&lt;/strong&gt; Explicit, numeric rules for "we extract this module into a service when this metric crosses X."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the evolution path open.&lt;/strong&gt; Event-first communication drastically lowers the cost of extracting a module into a service later.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  11. Conclusion — The Common Sense of Architecture Has Shifted in 2026
&lt;/h2&gt;

&lt;p&gt;2026 is the year the "microservices = modern" equation broke for good. &lt;strong&gt;The modular monolith is not a retreat but an evolution.&lt;/strong&gt; When combined with domain-driven design, architectural fitness functions, event-first communication, and observability, it delivers — for the majority of production systems — &lt;strong&gt;lower operational cost, higher development velocity, and equivalent scalability&lt;/strong&gt; compared to microservices. Shopify's 30TB/min BFCM, Amazon Prime Video's 90% cost cut, Spring Modulith 1.4 going GA, and ArchUnit 1.3's maturity prove this is a &lt;strong&gt;structural shift&lt;/strong&gt;, not a passing trend.&lt;/p&gt;

&lt;p&gt;Starting in Q2 2026, ManoIT is moving all new enterprise engagements to a default architecture of &lt;strong&gt;modular monolith plus selective service extraction&lt;/strong&gt;. The goal is to reduce operational burden today while keeping the door open for any module to evolve into an independent service as the business grows — an &lt;strong&gt;Evolvable Architecture&lt;/strong&gt;. Complexity should be justified by necessity, not by tooling.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was produced through ManoIT's Claude Opus 4.6–powered automated blog pipeline, with facts cross-checked against Spring Modulith's official blog, Shopify Engineering, the CNCF × SlashData 2026 report, Sam Newman's second edition of *Monolith to Microservices&lt;/em&gt;, Neal Ford's QCon 2026 keynote, the ArchUnit documentation, The New Stack, InfoQ, and dev.to. Benchmarks (Shopify's 30TB/min, the 42% consolidation rate, etc.) reflect each source's published measurements. Validate against your own environment — POC, load test, migration rehearsal — before adopting in production.*&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="http://www.manoit.co.kr/forum/view/1456526" rel="noopener noreferrer"&gt;ManoIT Tech Blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>softwareengineering</category>
      <category>programming</category>
      <category>backend</category>
    </item>
    <item>
      <title>Offline Hash Cracking Tutorial: Crack the Hash Room Walkthrough | TryHackMe</title>
      <dc:creator>Md. Ibrahim Reza Rabbi</dc:creator>
      <pubDate>Sun, 19 Apr 2026 09:04:19 +0000</pubDate>
      <link>https://forem.com/ibrahim71reza/offline-hash-cracking-tutorial-crack-the-hash-room-walkthrough-tryhackme-9be</link>
      <guid>https://forem.com/ibrahim71reza/offline-hash-cracking-tutorial-crack-the-hash-room-walkthrough-tryhackme-9be</guid>
      <description>&lt;p&gt;Now, We will jump to the 2nd level of this -&amp;gt;&lt;/p&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%2Fg6i82lhjvxmreakum3y4.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%2Fg6i82lhjvxmreakum3y4.png" alt=" " width="754" height="120"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Question-1:&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hash: F09EDCB1FCEFC6DFB23DC3505A882655FF77375ED8AA2D1C13F640FCCC2D0C85
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F4924p0shq1bxic7d2lwl.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%2F4924p0shq1bxic7d2lwl.png" alt=" " width="800" height="246"&gt;&lt;/a&gt;&lt;br&gt;
Now, lets crack this with SHA-256 mode 1400 by hashcat&lt;br&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%2Fvdjybfveafca856z3axh.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%2Fvdjybfveafca856z3axh.png" alt=" " width="614" height="423"&gt;&lt;/a&gt;&lt;br&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%2Ffslicgyjvtisy79zn2qs.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%2Ffslicgyjvtisy79zn2qs.png" alt=" " width="616" height="418"&gt;&lt;/a&gt;&lt;/p&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%2Fibdfdlesxywn45jf289h.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%2Fibdfdlesxywn45jf289h.png" alt=" " width="800" height="107"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;&lt;em&gt;Question-2:&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hash: 1DFECA0C002AE40B8619ECF94819CC1B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, this hash is tricky though it is showing MD5 or any version of MD but it is "NTLM". So, we should not blindly trust the top guess of this tools rather than sequentially test all the hash until we will get the hash cracked.&lt;/p&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%2F33y1clco38tx2kwo5slm.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%2F33y1clco38tx2kwo5slm.png" alt=" " width="800" height="215"&gt;&lt;/a&gt;&lt;/p&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%2Fox16bl1kqq65mhv5p5hl.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%2Fox16bl1kqq65mhv5p5hl.png" alt=" " width="800" height="243"&gt;&lt;/a&gt;&lt;/p&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%2Fb8b9s3r4dpeaef6kn0hf.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%2Fb8b9s3r4dpeaef6kn0hf.png" alt=" " width="636" height="386"&gt;&lt;/a&gt;&lt;/p&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%2Fdzq1dc58zznl2s5c73r8.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%2Fdzq1dc58zznl2s5c73r8.png" alt=" " width="800" height="105"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Question-3:&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hash: $6$aReallyHardSalt$6WKUTqzq.UQQmrm0p/T7MPpMbGNnzXPMAXi4bJMl9be.cfi3/qxIf.hsGpS41BqMhSrHVXgMpdjS6xeKZAs02.

Salt: aReallyHardSalt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F3taozp0qixymiibic6nh.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%2F3taozp0qixymiibic6nh.png" alt=" " width="800" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, it is bit tricky. Go to &lt;a href="https://hashcat.net/wiki/doku.php?id=example_hashes" rel="noopener noreferrer"&gt;hashcat_wiki&lt;/a&gt; and search the $6$ tag and understand which mode is this. -&amp;gt;&lt;/p&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%2Fbpz5fqyw9ppl7xnb84ih.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%2Fbpz5fqyw9ppl7xnb84ih.png" alt=" " width="800" height="106"&gt;&lt;/a&gt;&lt;br&gt;
okay now lets crack we don't need to add the salt in the hash manually cause it is attached with that in the hash. But, most of we miss to add the (.) full stop at the end. This full stop is a part of this hash. And also it will take some time to crack -&amp;gt;&lt;/p&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%2F5edsbhu979uyxs1usayu.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%2F5edsbhu979uyxs1usayu.png" alt=" " width="690" height="346"&gt;&lt;/a&gt;&lt;/p&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%2F2pge42x3dqk00y24cn0b.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%2F2pge42x3dqk00y24cn0b.png" alt=" " width="694" height="403"&gt;&lt;/a&gt;&lt;/p&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%2Fc0kjsd0jpplb3hl15yrg.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%2Fc0kjsd0jpplb3hl15yrg.png" alt=" " width="800" height="161"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;&lt;em&gt;Question-4:&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hash: e5d8870e5bdd26602cab8dbe07a942c8669e56d6
Salt: tryhackme
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F0t083ai20yxi5ah2jnt8.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%2F0t083ai20yxi5ah2jnt8.png" alt=" " width="800" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;now if we look at the &lt;a href="https://hashcat.net/wiki/doku.php?id=example_hashes" rel="noopener noreferrer"&gt;hashcat_wiki&lt;/a&gt; the Sha-1 with salt is the mode 110 and also see the format   sha1($pass.$salt) -&amp;gt;&lt;/p&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%2F60gjxkny7jtq8hzy58xg.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%2F60gjxkny7jtq8hzy58xg.png" alt=" " width="800" height="88"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But, unfortunately it didn't work :) then I sequentially search for other sha1 and salt type hash mode and I found this -&amp;gt;&lt;/p&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%2Fhaop9hwoi0pe25veswgx.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%2Fhaop9hwoi0pe25veswgx.png" alt=" " width="800" height="236"&gt;&lt;/a&gt;&lt;br&gt;
And with that 160 mode we cracked the hash -&amp;gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;┌──(kali㉿kali)-[~/password]
&lt;/span&gt;&lt;span class="gp"&gt;└─$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'e5d8870e5bdd26602cab8dbe07a942c8669e56d6:tryhackme'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; hash.txt
&lt;span class="go"&gt;
┌──(kali㉿kali)-[~/password]
&lt;/span&gt;&lt;span class="gp"&gt;└─$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;hashcat &lt;span class="nt"&gt;-m&lt;/span&gt; 160 &lt;span class="nt"&gt;-a&lt;/span&gt; 0 hash.txt /usr/share/wordlists/rockyou.txt      
&lt;span class="go"&gt;hashcat (v7.1.2) starting

&lt;/span&gt;&lt;span class="gp"&gt;OpenCL API (OpenCL 3.0 PoCL 6.0+debian  Linux, None+Asserts, RELOC, SPIR-V, LLVM 18.1.8, SLEEF, DISTRO, POCL_DEBUG) - Platform #&lt;/span&gt;1 &lt;span class="o"&gt;[&lt;/span&gt;The pocl project]
&lt;span class="go"&gt;====================================================================================================================================================
&lt;/span&gt;&lt;span class="gp"&gt;* Device #&lt;/span&gt;01: cpu-sandybridge-12th Gen Intel&lt;span class="o"&gt;(&lt;/span&gt;R&lt;span class="o"&gt;)&lt;/span&gt; Core&lt;span class="o"&gt;(&lt;/span&gt;TM&lt;span class="o"&gt;)&lt;/span&gt; i5-12450H, 1466/2933 MB &lt;span class="o"&gt;(&lt;/span&gt;512 MB allocatable&lt;span class="o"&gt;)&lt;/span&gt;, 4MCU
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F64t6l6oqn9xywg4lxl8z.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%2F64t6l6oqn9xywg4lxl8z.png" alt=" " width="639" height="387"&gt;&lt;/a&gt;&lt;/p&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%2F5e2pgyz1nzwc6sjgubm2.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%2F5e2pgyz1nzwc6sjgubm2.png" alt=" " width="800" height="155"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tryhackme</category>
      <category>cracking</category>
      <category>password</category>
      <category>linux</category>
    </item>
    <item>
      <title>Change tracking and soft delete: audit trails without the boilerplate</title>
      <dc:creator>David Lastrucci</dc:creator>
      <pubDate>Sun, 19 Apr 2026 09:02:53 +0000</pubDate>
      <link>https://forem.com/davidlastrucci/change-tracking-and-soft-delete-audit-trails-without-the-boilerplate-57oc</link>
      <guid>https://forem.com/davidlastrucci/change-tracking-and-soft-delete-audit-trails-without-the-boilerplate-57oc</guid>
      <description>&lt;p&gt;In most business applications, you need to answer questions like: &lt;em&gt;Who created this record? When was it last modified? Can we undo this deletion?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Implementing this by hand means adding columns, writing triggers or hooks, and remembering to update them on every operation. Trysil does it with six attributes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The change tracking attributes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Set on&lt;/th&gt;
&lt;th&gt;Required field type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[TCreatedAt]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Insert&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TTNullable&amp;lt;TDateTime&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[TCreatedBy]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Insert&lt;/td&gt;
&lt;td&gt;&lt;code&gt;String&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[TUpdatedAt]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TTNullable&amp;lt;TDateTime&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[TUpdatedBy]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update&lt;/td&gt;
&lt;td&gt;&lt;code&gt;String&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[TDeletedAt]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TTNullable&amp;lt;TDateTime&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[TDeletedBy]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete&lt;/td&gt;
&lt;td&gt;&lt;code&gt;String&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You add them to your entity fields, and Trysil fills them in automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding an audit trail
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unit Article.Model;

{$WARN UNKNOWN_CUSTOM_ATTRIBUTE ERROR}

interface

uses
  Trysil.Types,
  Trysil.Attributes,
  Trysil.Validation.Attributes;

type
  [TTable('Articles')]
  [TSequence('ArticlesID')]
  TTArticle = class
  strict private
    [TPrimaryKey]
    [TColumn('ID')]
    FID: TTPrimaryKey;

    [TRequired]
    [TMaxLength(200)]
    [TColumn('Title')]
    FTitle: String;

    [TColumn('Body')]
    FBody: String;

    [TCreatedAt]
    [TColumn('CreatedAt')]
    FCreatedAt: TTNullable&amp;lt;TDateTime&amp;gt;;

    [TCreatedBy]
    [TColumn('CreatedBy')]
    FCreatedBy: String;

    [TUpdatedAt]
    [TColumn('UpdatedAt')]
    FUpdatedAt: TTNullable&amp;lt;TDateTime&amp;gt;;

    [TUpdatedBy]
    [TColumn('UpdatedBy')]
    FUpdatedBy: String;

    [TColumn('VersionID')]
    [TVersionColumn]
    FVersionID: TTVersion;
  public
    property ID: TTPrimaryKey read FID;
    property Title: String read FTitle write FTitle;
    property Body: String read FBody write FBody;
    property CreatedAt: TTNullable&amp;lt;TDateTime&amp;gt; read FCreatedAt;
    property CreatedBy: String read FCreatedBy;
    property UpdatedAt: TTNullable&amp;lt;TDateTime&amp;gt; read FUpdatedAt;
    property UpdatedBy: String read FUpdatedBy;
  end;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when you insert or update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var
  LArticle: TTArticle;
begin
  LArticle := LContext.CreateEntity&amp;lt;TTArticle&amp;gt;();
  LArticle.Title := 'Getting started with Trysil';
  LArticle.Body := 'In this article...';
  LContext.Insert&amp;lt;TTArticle&amp;gt;(LArticle);
  // CreatedAt is now set to the current timestamp
  // CreatedBy is set to the current user name
end;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LArticle.Title := 'Getting started with Trysil (updated)';
LContext.Update&amp;lt;TTArticle&amp;gt;(LArticle);
// UpdatedAt is now set to the current timestamp
// UpdatedBy is set to the current user name
// CreatedAt and CreatedBy remain unchanged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Providing the current user
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;*By&lt;/code&gt; fields need to know &lt;em&gt;who&lt;/em&gt; the current user is. You provide this via a callback on &lt;code&gt;TTContext&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LContext.OnGetCurrentUser := function: String
begin
  result := 'david.lastrucci';
end;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a real application, this might read from an authentication token, a session variable, or a thread-local user context. If you do not assign the callback, Trysil writes an empty string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Soft delete
&lt;/h2&gt;

&lt;p&gt;Traditional DELETE removes the row from the database. In many scenarios — audit compliance, undo functionality, data recovery — you want to keep the record but mark it as deleted.&lt;/p&gt;

&lt;p&gt;Trysil supports this natively. Add &lt;code&gt;[TDeletedAt]&lt;/code&gt; (and optionally &lt;code&gt;[TDeletedBy]&lt;/code&gt;) to your entity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  [TTable('Articles')]
  [TSequence('ArticlesID')]
  TTArticle = class
  strict private
    // ... other fields ...

    [TDeletedAt]
    [TColumn('DeletedAt')]
    FDeletedAt: TTNullable&amp;lt;TDateTime&amp;gt;;

    [TDeletedBy]
    [TColumn('DeletedBy')]
    FDeletedBy: String;

    [TColumn('VersionID')]
    [TVersionColumn]
    FVersionID: TTVersion;
  public
    // ... properties ...
    property DeletedAt: TTNullable&amp;lt;TDateTime&amp;gt; read FDeletedAt;
    property DeletedBy: String read FDeletedBy;
  end;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What changes
&lt;/h3&gt;

&lt;p&gt;When an entity has a &lt;code&gt;[TDeletedAt]&lt;/code&gt; field, calling &lt;code&gt;Delete&amp;lt;T&amp;gt;&lt;/code&gt; no longer executes a SQL DELETE. Instead it executes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;Articles&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;DeletedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;DeletedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DeletedBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;DeletedBy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VersionID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VersionID&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;VersionID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;VersionID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The record stays in the database, but it is marked as deleted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic exclusion
&lt;/h3&gt;

&lt;p&gt;All SELECT queries on that entity automatically add &lt;code&gt;DeletedAt IS NULL&lt;/code&gt; to the WHERE clause. Soft-deleted records are invisible by default — your application code does not need to change at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// This only returns non-deleted articles
LContext.SelectAll&amp;lt;TTArticle&amp;gt;(LArticles);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Including deleted records
&lt;/h3&gt;

&lt;p&gt;Sometimes you need to see deleted records (admin panels, audit logs). Use the filter builder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var
  LBuilder: TTFilterBuilder&amp;lt;TTArticle&amp;gt;;
  LFilter: TTFilter;
begin
  LBuilder := LContext.CreateFilterBuilder&amp;lt;TTArticle&amp;gt;();
  try
    LFilter := LBuilder
      .IncludeDeleted
      .OrderByDesc('DeletedAt')
      .Build;

    LContext.Select&amp;lt;TTArticle&amp;gt;(LAllArticles, LFilter);
  finally
    LBuilder.Free;
  end;
end;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Relation checks and soft delete
&lt;/h3&gt;

&lt;p&gt;When you soft-delete a parent record, Trysil &lt;strong&gt;skips&lt;/strong&gt; the child relation check. This makes sense: the record is not being physically removed, so foreign key integrity is preserved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it all together
&lt;/h2&gt;

&lt;p&gt;Here is the SQL table that supports full change tracking with soft delete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;Articles&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;CreatedBy&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;UpdatedAt&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;UpdatedBy&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;DeletedAt&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;DeletedBy&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;VersionID&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the complete entity lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var
  LArticle: TTArticle;
begin
  // Create
  LArticle := LContext.CreateEntity&amp;lt;TTArticle&amp;gt;();
  LArticle.Title := 'My article';
  LArticle.Body := 'Content here';
  LContext.Insert&amp;lt;TTArticle&amp;gt;(LArticle);
  // CreatedAt = 2026-04-09 10:30:00, CreatedBy = 'david.lastrucci'

  // Update
  LArticle.Title := 'My article (revised)';
  LContext.Update&amp;lt;TTArticle&amp;gt;(LArticle);
  // UpdatedAt = 2026-04-09 11:15:00, UpdatedBy = 'david.lastrucci'

  // Soft delete
  LContext.Delete&amp;lt;TTArticle&amp;gt;(LArticle);
  // DeletedAt = 2026-04-09 14:00:00, DeletedBy = 'david.lastrucci'
  // Record is still in the database, but invisible to normal queries
end;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Series recap
&lt;/h2&gt;

&lt;p&gt;Over these six articles we have covered the core of Trysil:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;First contact&lt;/strong&gt; — entity, connection, CRUD&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entity mapping&lt;/strong&gt; — attributes, types, nullable fields, optimistic locking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation&lt;/strong&gt; — declarative rules, custom validators, error handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filtering&lt;/strong&gt; — fluent query builder, sorting, pagination&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relations&lt;/strong&gt; — lazy loading, cascade delete, parent-child patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change tracking&lt;/strong&gt; — audit trails, soft delete, automatic exclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Trysil also includes a &lt;strong&gt;JSON serialization module&lt;/strong&gt;, an &lt;strong&gt;HTTP/REST hosting module&lt;/strong&gt; with attribute-based routing and JWT authentication, and a &lt;strong&gt;Unit of Work&lt;/strong&gt; pattern via &lt;code&gt;TTSession&amp;lt;T&amp;gt;&lt;/code&gt;. These are topics for future articles.&lt;/p&gt;

&lt;p&gt;If you want to explore further, the &lt;a href="https://github.com/davidlastrucci/Trysil" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; contains full demo projects, a cookbook with 17 copy-paste recipes, and complete API documentation.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Trysil is open-source and available on &lt;a href="https://github.com/davidlastrucci/Trysil" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. If this series helped you, consider giving the project a star — it helps other Delphi developers discover it!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>delphi</category>
      <category>orm</category>
      <category>database</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Tu WebView no necesita un servidor HTTP. Necesita un Virtual Host.</title>
      <dc:creator>Giovani Fouz</dc:creator>
      <pubDate>Sun, 19 Apr 2026 09:01:38 +0000</pubDate>
      <link>https://forem.com/gfouz/tu-webview-no-necesita-un-servidor-http-necesita-un-virtual-host-3jca</link>
      <guid>https://forem.com/gfouz/tu-webview-no-necesita-un-servidor-http-necesita-un-virtual-host-3jca</guid>
      <description>&lt;h3&gt;
  
  
  Cómo un patrón de los años 90 resolvió uno de los problemas más frustrantes del desarrollo móvil moderno
&lt;/h3&gt;




&lt;p&gt;Hay una idea en ingeniería de software que reaparece una y otra vez, con décadas de distancia y en plataformas completamente distintas. Una idea tan elegante que la industria la redescubre cada cierto tiempo, la viste con nueva ropa, y la presenta como si fuera nueva.&lt;/p&gt;

&lt;p&gt;Esta es la historia de esa idea. Y de cómo terminó viviendo dentro de una app Android.&lt;/p&gt;




&lt;h2&gt;
  
  
  El problema que nadie quiere admitir
&lt;/h2&gt;

&lt;p&gt;Construir una app móvil con React, Vue o cualquier framework web moderno es tentador. Tienes un equipo que ya sabe web, una base de código compartida, y la promesa de "escribe una vez, corre en todas partes."&lt;/p&gt;

&lt;p&gt;Hasta que llega el momento de cargar tu app en Android sin conexión a internet.&lt;/p&gt;

&lt;p&gt;Android no tiene un servidor local. WebView no sabe hablar con tu carpeta &lt;code&gt;assets/&lt;/code&gt; como si fuera un dominio real. Y React Router — el corazón de la navegación en casi toda SPA moderna — necesita que las rutas como &lt;code&gt;/dashboard&lt;/code&gt; o &lt;code&gt;/perfil/123&lt;/code&gt; devuelvan siempre el mismo &lt;code&gt;index.html&lt;/code&gt;, sin importar qué tan profunda sea la URL.&lt;/p&gt;

&lt;p&gt;Sin eso, tu app se rompe. El usuario navega, presiona atrás, recarga — y ve una pantalla en blanco o un error 404 que no debería existir.&lt;/p&gt;

&lt;p&gt;La solución obvia sería levantar un servidor HTTP local. Pero eso consume batería, requiere permisos, complica el ciclo de vida de la app, y abre vectores de seguridad innecesarios. No es la respuesta correcta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La respuesta correcta tiene 30 años de historia detrás.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1991: Cuando los servidores aprendieron a mentir (bien)
&lt;/h2&gt;

&lt;p&gt;En los primeros días de la web, cada URL correspondía a un archivo físico en el disco del servidor. &lt;code&gt;/pagina.html&lt;/code&gt; era literalmente un archivo llamado &lt;code&gt;pagina.html&lt;/code&gt;. Simple, predecible, rígido.&lt;/p&gt;

&lt;p&gt;El problema llegó cuando los sitios crecieron. Un servidor físico, múltiples dominios. ¿Cómo diferenciaba el servidor a cuál de ellos le hablaba el visitante?&lt;/p&gt;

&lt;p&gt;La respuesta fue el &lt;strong&gt;Virtual Hosting&lt;/strong&gt;: la capacidad de un solo servidor de hacerse pasar por muchos, respondiendo diferente según el dominio que el cliente pedía. Apache HTTP Server lo popularizó a mediados de los 90. Un servidor físico podía alojar &lt;code&gt;empresa-a.com&lt;/code&gt;, &lt;code&gt;empresa-b.com&lt;/code&gt; y &lt;code&gt;empresa-c.com&lt;/code&gt; simultáneamente, cada uno con su propio espacio de archivos, sus propias reglas, su propia identidad.&lt;/p&gt;

&lt;p&gt;Era, en esencia, enseñarle a un servidor a interceptar una petición y decidir: &lt;em&gt;¿quién eres tú y qué te mereces recibir?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Nginx lo refinó. Los CDNs lo escalaron a millones de dominios. Y el patrón quedó grabado en el ADN de la infraestructura web moderna.&lt;/p&gt;




&lt;h2&gt;
  
  
  2010: Las SPAs rompen todo (de nuevo)
&lt;/h2&gt;

&lt;p&gt;Cuando llegaron las Single Page Applications, el Virtual Hosting tuvo que evolucionar.&lt;/p&gt;

&lt;p&gt;Una SPA tiene una sola entrada: &lt;code&gt;index.html&lt;/code&gt;. Todo lo demás — rutas, vistas, estados — es JavaScript puro que corre en el navegador. El servidor no sabe nada de &lt;code&gt;/dashboard&lt;/code&gt; o &lt;code&gt;/usuario/perfil&lt;/code&gt;. Cuando alguien recarga esa URL, el servidor busca un archivo llamado &lt;code&gt;dashboard&lt;/code&gt; en el disco. No existe. 404.&lt;/p&gt;

&lt;p&gt;La solución fue tan simple como poderosa: &lt;strong&gt;el fallback a index.html&lt;/strong&gt;. Si el servidor no encuentra el archivo estático solicitado, en lugar de responder con un error, devuelve siempre &lt;code&gt;index.html&lt;/code&gt;. React Router toma el control desde ahí y reconstruye la vista correcta.&lt;/p&gt;

&lt;p&gt;En Nginx, son dos líneas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;En ASP.NET Core, Microsoft lo formalizó como middleware oficial con MapFallbackToFile("index.html"). El patrón tenía nombre, documentación, y respaldo institucional.&lt;/p&gt;

&lt;p&gt;Pero todo eso asume que tienes un servidor. ¿Qué pasa cuando no lo tienes?&lt;/p&gt;



&lt;p&gt;2024: El mismo patrón, sin servidor&lt;/p&gt;

&lt;p&gt;Android tiene un mecanismo poco conocido pero extraordinariamente poderoso: shouldInterceptRequest. Cada vez que el WebView está a punto de hacer una petición de red, se detiene y pregunta: ¿alguien quiere manejar esto antes que yo?&lt;/p&gt;

&lt;p&gt;La mayoría de los desarrolladores lo ignora. Algunos lo usan para bloquear anuncios o inyectar headers. Pero hay una tercera posibilidad: usarlo para construir un servidor virtual completo que nunca hace una petición de red real.&lt;/p&gt;

&lt;p&gt;Eso es exactamente lo que hace VirtualHostManager.&lt;/p&gt;

&lt;p&gt;Cuando el WebView intenta cargar &lt;a href="https://app.gfouz.com/assets/main.js" rel="noopener noreferrer"&gt;https://app.gfouz.com/assets/main.js&lt;/a&gt;, la clase intercepta esa petición antes de que salga al mundo. Busca el archivo en la carpeta assets/ del APK. Lo sirve directamente, con el MIME type correcto, con los headers de caché apropiados, sin tocar internet.&lt;/p&gt;

&lt;p&gt;Y cuando el WebView pide &lt;a href="https://app.gfouz.com/dashboard" rel="noopener noreferrer"&gt;https://app.gfouz.com/dashboard&lt;/a&gt; — una ruta de React Router que no existe como archivo — la clase reconoce que no tiene extensión, asume que es una ruta SPA, y devuelve index.html. React Router hace el resto.&lt;/p&gt;

&lt;p&gt;El servidor está dentro de la app. El dominio es ficticio. Las peticiones nunca salen del dispositivo. Y React Router nunca se entera de la diferencia.&lt;/p&gt;



&lt;p&gt;Por qué esto importa más de lo que parece&lt;/p&gt;

&lt;p&gt;La mayoría de soluciones para este problema siguen uno de dos caminos: usar file:// (que rompe CORS y tiene restricciones de seguridad severas) o levantar un servidor HTTP local con librerías como NanoHTTPD (que consume recursos y complica el ciclo de vida de la app).&lt;/p&gt;

&lt;p&gt;VirtualHostManager toma un tercer camino: habitar la infraestructura existente del WebView en lugar de luchar contra ella o rodearla.&lt;/p&gt;

&lt;p&gt;El resultado es una clase que:&lt;/p&gt;

&lt;p&gt;· No abre puertos de red.&lt;br&gt;
· No requiere permisos adicionales.&lt;br&gt;
· Cachea solo lo que vale cachear (index.html, que se pide en cada navegación).&lt;br&gt;
· Transmite archivos grandes como streams, sin cargarlos completos en RAM.&lt;br&gt;
· Valida rutas para prevenir path traversal attacks.&lt;br&gt;
· Soporta 25 tipos de archivo con sus MIME types correctos.&lt;br&gt;
· Funciona en Android 4.1 en adelante.&lt;/p&gt;

&lt;p&gt;Y hace todo eso en menos de 200 líneas de Java.&lt;/p&gt;



&lt;p&gt;El patrón que no envejece&lt;/p&gt;

&lt;p&gt;Lo fascinante de esta historia no es la clase en sí. Es que el problema — ¿cómo sirvo contenido estático con inteligencia? — ha tenido exactamente la misma solución en cada era de la computación:&lt;/p&gt;

&lt;p&gt;1995 → Apache Virtual Hosting&lt;br&gt;
2005 → Nginx try_files&lt;br&gt;
2016 → ASP.NET Core MapFallbackToFile&lt;br&gt;
2024 → Android shouldInterceptRequest + SPA fallback&lt;/p&gt;

&lt;p&gt;El contexto cambia. El hardware cambia. Los frameworks cambian. El patrón permanece.&lt;/p&gt;

&lt;p&gt;Eso, en ingeniería de software, es la señal más confiable de que algo es fundamentalmente correcto.&lt;/p&gt;

&lt;p&gt;VirtualHostManager es parte de FouzStack — una colección de soluciones de ingeniería para desarrollo móvil con tecnologías web.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://github.com/gfouz" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F82347049%3Fv%3D4%3Fs%3D400" height="300" class="m-0" width="300"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://github.com/gfouz" rel="noopener noreferrer" class="c-link"&gt;
            gfouz (Giovani Fouz) · GitHub
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Welcome,  I'm excited to share my journey,
skills, and projects with you. Dive in, explore
my projects, and get to know the developer behind
the screen. - gfouz
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.githubassets.com%2Ffavicons%2Ffavicon.svg" width="32" height="32"&gt;
          github.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;





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

&lt;/div&gt;

</description>
      <category>android</category>
      <category>architecture</category>
      <category>mobile</category>
      <category>spanish</category>
    </item>
    <item>
      <title>How to Containerize Your Python App and Deploy to VPS</title>
      <dc:creator>Big Mazzy</dc:creator>
      <pubDate>Sun, 19 Apr 2026 09:00:13 +0000</pubDate>
      <link>https://forem.com/big_mazzy_06d057cc24398c5/how-to-containerize-your-python-app-and-deploy-to-vps-4bl8</link>
      <guid>https://forem.com/big_mazzy_06d057cc24398c5/how-to-containerize-your-python-app-and-deploy-to-vps-4bl8</guid>
      <description>&lt;p&gt;Are you tired of the "it works on my machine" problem when deploying your Python applications? Containerization offers a solution by packaging your application and its dependencies into a portable unit. This article will guide you through containerizing your Python app using Docker and deploying it to a Virtual Private Server (VPS).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Containerize Your Python App?
&lt;/h2&gt;

&lt;p&gt;Deploying applications can be a complex process. Different environments often have varying system libraries, Python versions, or installed packages, leading to unexpected errors. Containerization, using tools like Docker, solves this by bundling your application with everything it needs to run. This ensures consistency across development, testing, and production environments.&lt;/p&gt;

&lt;p&gt;Think of a container like a self-contained apartment. It has its own plumbing, electricity, and furniture – everything needed for a resident to live comfortably, without interfering with other apartments or the building's main infrastructure. Similarly, a Docker container includes your application, its runtime, system tools, libraries, and settings, all isolated from the host system and other containers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Docker
&lt;/h2&gt;

&lt;p&gt;Docker is an open-source platform that automates the deployment, scaling, and management of applications using containers. It provides a standardized way to package applications, ensuring they run reliably across different computing environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a Docker Image?
&lt;/h3&gt;

&lt;p&gt;A Docker image is a lightweight, standalone, executable package that includes everything needed to run a piece of software, including the code, runtime, system tools, system libraries, and settings. Images are immutable, meaning they cannot be changed after they are built.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a Docker Container?
&lt;/h3&gt;

&lt;p&gt;A Docker container is a runnable instance of a Docker image. You can create, start, stop, and delete containers. A container is the actual running process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started with Docker
&lt;/h2&gt;

&lt;p&gt;Before you can containerize your Python app, you need to install Docker on your development machine. You can find installation instructions for various operating systems on the official Docker website.&lt;/p&gt;

&lt;p&gt;Once Docker is installed, you can start building your container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a Dockerfile
&lt;/h2&gt;

&lt;p&gt;A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image. Docker reads this file and executes the commands sequentially to build the image.&lt;/p&gt;

&lt;p&gt;Let's create a simple Flask application to demonstrate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;app.py&lt;/code&gt;:&lt;/strong&gt;&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hello_world&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Hello from my containerized Python app!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a basic Flask web application that will serve a simple greeting. The &lt;code&gt;host='0.0.0.0'&lt;/code&gt; is crucial for allowing the application to be accessible from outside the container.&lt;/p&gt;

&lt;p&gt;Now, let's create our &lt;code&gt;Dockerfile&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Dockerfile&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Use an official Python runtime as a parent image&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.9-slim&lt;/span&gt;

&lt;span class="c"&gt;# Set the working directory in the container&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Copy the current directory contents into the container at /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . /app&lt;/span&gt;

&lt;span class="c"&gt;# Install any needed packages specified in requirements.txt&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# Make port 5000 available to the world outside this container&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 5000&lt;/span&gt;

&lt;span class="c"&gt;# Define environment variable&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NAME World&lt;/span&gt;

&lt;span class="c"&gt;# Run app.py when the container launches&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "app.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break down this &lt;code&gt;Dockerfile&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;FROM python:3.9-slim&lt;/code&gt;: This specifies the base image. We're using a lightweight official Python 3.9 image. Using &lt;code&gt;-slim&lt;/code&gt; variants often results in smaller image sizes.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;WORKDIR /app&lt;/code&gt;: This sets the working directory inside the container. All subsequent commands will be executed from this directory.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;COPY . /app&lt;/code&gt;: This copies the files from your local project directory (where the &lt;code&gt;Dockerfile&lt;/code&gt; is located) into the &lt;code&gt;/app&lt;/code&gt; directory inside the container.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;RUN pip install --no-cache-dir -r requirements.txt&lt;/code&gt;: This command installs the Python dependencies. We assume you have a &lt;code&gt;requirements.txt&lt;/code&gt; file. The &lt;code&gt;--no-cache-dir&lt;/code&gt; flag helps reduce the image size by not storing the pip cache.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;EXPOSE 5000&lt;/code&gt;: This informs Docker that the container will listen on port 5000 at runtime. It's documentation and doesn't actually publish the port.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;ENV NAME World&lt;/code&gt;: This sets an environment variable named &lt;code&gt;NAME&lt;/code&gt; with the value &lt;code&gt;World&lt;/code&gt;. This is an example of how you can pass configuration into your container.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;CMD ["python", "app.py"]&lt;/code&gt;: This specifies the command to run when the container starts. It executes our Flask application.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Creating &lt;code&gt;requirements.txt&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;You'll need a &lt;code&gt;requirements.txt&lt;/code&gt; file listing your Python dependencies. For our example, it would be:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;requirements.txt&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Flask==2.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Building the Docker Image
&lt;/h2&gt;

&lt;p&gt;Navigate to your project directory in your terminal (the one containing &lt;code&gt;app.py&lt;/code&gt;, &lt;code&gt;Dockerfile&lt;/code&gt;, and &lt;code&gt;requirements.txt&lt;/code&gt;). Then, build the Docker image using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; my-python-app &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;docker build&lt;/code&gt;: This command initiates the image building process.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;-t my-python-app&lt;/code&gt;: The &lt;code&gt;-t&lt;/code&gt; flag tags the image with a name (&lt;code&gt;my-python-app&lt;/code&gt;). You can choose any name.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;.&lt;/code&gt;: This dot indicates that the &lt;code&gt;Dockerfile&lt;/code&gt; is in the current directory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This command will execute the steps in your &lt;code&gt;Dockerfile&lt;/code&gt;, download the base image, copy your code, install dependencies, and create your custom image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the Docker Container Locally
&lt;/h2&gt;

&lt;p&gt;Before deploying, it's a good practice to test your container locally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 5000:5000 my-python-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;docker run&lt;/code&gt;: This command creates and starts a new container from an image.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;-p 5000:5000&lt;/code&gt;: This maps port 5000 on your host machine to port 5000 inside the container. The format is &lt;code&gt;host_port:container_port&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;my-python-app&lt;/code&gt;: This is the name of the image you want to run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, open your web browser and go to &lt;code&gt;http://localhost:5000&lt;/code&gt;. You should see "Hello from my containerized Python app!".&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying to a VPS
&lt;/h2&gt;

&lt;p&gt;To deploy your application to the internet, you'll need a Virtual Private Server (VPS). A VPS is a virtual machine sold as a service by an internet hosting service. It provides dedicated resources like CPU, RAM, and storage, giving you more control than shared hosting.&lt;/p&gt;

&lt;p&gt;When choosing a VPS provider, consider factors like performance, pricing, and support. Providers like &lt;a href="https://powervps.net/?from=32" rel="noopener noreferrer"&gt;PowerVPS&lt;/a&gt; offer competitive pricing and robust infrastructure, making them a good option for hosting containerized applications. Another excellent choice is &lt;a href="https://en.immers.cloud/signup/r/20241007-8310688-334/" rel="noopener noreferrer"&gt;Immers Cloud&lt;/a&gt;, which provides flexible plans suitable for various deployment needs. For a comprehensive overview of server rental options, the &lt;a href="https://serverrental.store" rel="noopener noreferrer"&gt;Server Rental Guide&lt;/a&gt; is a valuable resource.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up Your VPS
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Provision a VPS:&lt;/strong&gt; Choose a Linux distribution (like Ubuntu or Debian) and provision your VPS.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Connect via SSH:&lt;/strong&gt; Securely connect to your VPS using SSH.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install Docker:&lt;/strong&gt; Install Docker on your VPS. The installation process will vary slightly depending on your VPS's operating system. For Ubuntu, you can typically use:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;docker.io &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start docker
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;docker
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;It's also a good idea to add your user to the &lt;code&gt;docker&lt;/code&gt; group to run Docker commands without &lt;code&gt;sudo&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="nv"&gt;$USER&lt;/span&gt;
newgrp docker &lt;span class="c"&gt;# Apply group changes to the current session&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Transferring Your Application
&lt;/h3&gt;

&lt;p&gt;You can transfer your application files and &lt;code&gt;Dockerfile&lt;/code&gt; to the VPS using &lt;code&gt;scp&lt;/code&gt; or by cloning your Git repository.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;scp&lt;/code&gt; (example):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On your local machine&lt;/span&gt;
scp &lt;span class="nt"&gt;-r&lt;/span&gt; /path/to/your/app/directory user@your_vps_ip:/home/user/app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;/path/to/your/app/directory&lt;/code&gt; with the actual path on your local machine, &lt;code&gt;user&lt;/code&gt; with your VPS username, and &lt;code&gt;your_vps_ip&lt;/code&gt; with your VPS's IP address.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building the Image on the VPS
&lt;/h3&gt;

&lt;p&gt;Once your files are on the VPS, navigate to your application directory and build the Docker image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /home/user/app &lt;span class="c"&gt;# Or wherever you copied your app&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; my-python-app &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running the Container on the VPS
&lt;/h3&gt;

&lt;p&gt;Now, run your container on the VPS. You'll want to map a port on the VPS to your container's port. For public accessibility, you'll typically use port 80 (HTTP) or 443 (HTTPS).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 80:5000 my-python-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;-d&lt;/code&gt;: This runs the container in detached mode, meaning it will run in the background.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;-p 80:5000&lt;/code&gt;: This maps port 80 on your VPS to port 5000 inside the container.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, you should be able to access your application by navigating to your VPS's IP address in a web browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Considerations for Production
&lt;/h2&gt;

&lt;p&gt;While the above steps get your application running, production deployments often require more robust solutions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Port Mapping and Firewalls
&lt;/h3&gt;

&lt;p&gt;Ensure your VPS's firewall is configured to allow traffic on the port you're exposing (e.g., port 80). If you're using a cloud provider's firewall, you'll need to open the relevant ports there as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reverse Proxy (Nginx/Apache)
&lt;/h3&gt;

&lt;p&gt;For production, it's common to use a reverse proxy like Nginx. A reverse proxy sits in front of your application container, handles incoming requests, and forwards them to your application. This offers benefits like SSL termination, load balancing, and caching.&lt;/p&gt;

&lt;p&gt;You would typically run Nginx in its own Docker container and configure it to proxy requests to your Python application container.&lt;/p&gt;

&lt;h3&gt;
  
  
  Persistent Storage
&lt;/h3&gt;

&lt;p&gt;If your application needs to store data (e.g., user uploads, database files), you'll need to use Docker volumes or bind mounts to persist data outside the container's lifecycle. Otherwise, any data stored inside the container will be lost when the container is removed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Container Orchestration
&lt;/h3&gt;

&lt;p&gt;For more complex deployments with multiple containers, microservices, or the need for automatic scaling and self-healing, consider container orchestration platforms like Docker Swarm or Kubernetes. These tools manage the deployment, scaling, and networking of containerized applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Containerizing your Python applications with Docker and deploying them to a VPS provides a consistent, reliable, and scalable way to run your code. By following these steps, you can move beyond the "it works on my machine" dilemma and gain better control over your application's deployment environment. Remember to prioritize security and consider advanced deployment strategies for production-ready applications.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>cloud</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Idempotency in CQRS and Event Sourcing — Part 2: commands, projections and outbox</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Sun, 19 Apr 2026 09:00:02 +0000</pubDate>
      <link>https://forem.com/ohugonnot/idempotency-in-cqrs-and-event-sourcing-part-2-commands-projections-and-outbox-4ei</link>
      <guid>https://forem.com/ohugonnot/idempotency-in-cqrs-and-event-sourcing-part-2-commands-projections-and-outbox-4ei</guid>
      <description>&lt;p&gt;In HTTP, idempotency is straightforward: store the key, return the cache. In a CQRS/Event Sourcing system, it's more subtle. The command may be idempotent, but what about the event it generates? The projection consuming it? Idempotency must cross the entire stack.&lt;/p&gt;

&lt;p&gt;If you haven't read part 1, it covers the basics and the HTTP implementation in Go — with a complete PostgreSQL store that replaces Redis. Here we go one level deeper: command store, optimistic locking, idempotent projections, outbox pattern. These are the patterns found in financial systems, e-commerce platforms, anywhere a processing duplicate costs money or trust. Everything runs on PostgreSQL — no Redis, no external broker for idempotency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem specific to Event Sourcing
&lt;/h2&gt;

&lt;p&gt;In Event Sourcing, the application state &lt;em&gt;is&lt;/em&gt; the event stream. An account balance is not a column in the database — it's the sum of &lt;code&gt;AccountCredited&lt;/code&gt; and &lt;code&gt;AccountDebited&lt;/code&gt; events since opening. If the same event is recorded twice, the reconstructed state is wrong. And often irreparable without manual intervention.&lt;/p&gt;

&lt;p&gt;Concrete scenario on a payment system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The command &lt;code&gt;DebitAccount(amount=100, idempotencyKey=abc123)&lt;/code&gt; arrives&lt;/li&gt;
&lt;li&gt;  The event &lt;code&gt;AccountDebited(amount=100)&lt;/code&gt; is recorded, balance goes from 200 to 100&lt;/li&gt;
&lt;li&gt;  Network drops. Client retries. The same command arrives a second time&lt;/li&gt;
&lt;li&gt;  Without protection: a second &lt;code&gt;AccountDebited(amount=100)&lt;/code&gt; is recorded, balance drops to 0&lt;/li&gt;
&lt;li&gt;  The user has been charged twice for a single purchase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a classic relational database, you could fix this with an UPDATE. In Event Sourcing, you never modify the past — you can only append a corrective event, which complicates audit and support. Better to prevent the duplicate from getting in at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Command idempotency with the idempotency key
&lt;/h2&gt;

&lt;p&gt;The principle is the same as in HTTP: associate each command with a unique key provided by the client, and check before processing whether that key has already been seen. The difference is that you store the result in a dedicated &lt;em&gt;command store&lt;/em&gt;, and the save must be atomic with the events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;processed_commands&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;idempotency_key&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aggregate_id&lt;/span&gt;    &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;command_type&lt;/span&gt;    &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;result_event_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;processed_at&lt;/span&gt;    &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&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;The handler follows a three-step schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;PaymentHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="n"&gt;DébiterCompte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// 1. Check if already processed&lt;/span&gt;
    &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commandStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IdempotencyKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Already processed — return silent success&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&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;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrNotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"checking idempotency: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// 2. Process the command&lt;/span&gt;
    &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aggregate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// 3. Save events + mark command as processed — ATOMICALLY&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SaveEventsAndMarkCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IdempotencyKey&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;The atomicity of step 3 is non-negotiable. If you save the events in one transaction and mark the command in a second separate transaction, a crash between the two gives you: events in the database, command not marked. On the next retry, the handler doesn't see the key, processes again, records a duplicate event. Both operations must live in the same PostgreSQL transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimistic locking — detecting concurrent conflicts
&lt;/h2&gt;

&lt;p&gt;The idempotency key protects against duplicates of the same command. It doesn't protect against two &lt;em&gt;different&lt;/em&gt; commands modifying the same aggregate at the same time. That's the role of optimistic locking.&lt;/p&gt;

&lt;p&gt;Each aggregate has a version number. When a client reads the state of an aggregate, it receives its current version. When it sends a command, it includes that version. If in the meantime someone else has written to the same aggregate, the versions no longer match and the command is rejected — it's up to the client to re-read the state and decide whether its command still holds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;account_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;             &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aggregate_id&lt;/span&gt;   &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;version&lt;/span&gt;        &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt;     &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;        &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;     &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aggregate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;-- key constraint&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;UNIQUE(aggregate_id, version)&lt;/code&gt; constraint does all the work. If two concurrent commands try to write version 5 of the same aggregate, PostgreSQL lets one through and rejects the other with a uniqueness violation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;DébiterCompte&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;AggregateID&lt;/span&gt;     &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UUID&lt;/span&gt;
    &lt;span class="n"&gt;ExpectedVersion&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="c"&gt;// version the client read&lt;/span&gt;
    &lt;span class="n"&gt;Montant&lt;/span&gt;         &lt;span class="kt"&gt;float64&lt;/span&gt;
    &lt;span class="n"&gt;IdempotencyKey&lt;/span&gt;  &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UUID&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;EventStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;AppendEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aggregateID&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;expectedVersion&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;`INSERT INTO account_events (id, aggregate_id, version, event_type, payload)
             VALUES ($1, $2, $3, $4, $5)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;aggregateID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;expectedVersion&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payload&lt;/span&gt;&lt;span class="p"&gt;,&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;isUniqueViolation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ErrVersionConflict&lt;/span&gt; &lt;span class="c"&gt;// caller can retry with the new version&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"appending event: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By returning &lt;code&gt;ErrVersionConflict&lt;/code&gt;, we leave the decision to retry or surface the error to the user to the caller. In a financial system, you often surface an explicit error ("your operation was cancelled because the account was modified in the meantime") rather than retrying silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotent projections
&lt;/h2&gt;

&lt;p&gt;Projections consume the event stream to build denormalized views — the current balance of an account, the order list for a customer, etc. With Kafka or any at-least-once system, the same event can arrive multiple times. The projection must produce the same result whether it sees it once or ten times.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 1: tracking position in the stream
&lt;/h3&gt;

&lt;p&gt;Each projection remembers the ID of the last event it processed. Events older than or equal to the checkpoint are ignored.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;projection_checkpoints&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;projection_name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_event_id&lt;/span&gt;   &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;updated_at&lt;/span&gt;      &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&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;This approach works well when events are ordered and IDs can be compared (UUIDs v7 or sequences). It assumes the event store guarantees a stable order, which is generally true per aggregate but may be less so across different aggregates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 2: idempotent upsert
&lt;/h3&gt;

&lt;p&gt;Rather than filtering duplicates upstream, write in a way that rewriting the same data is harmless. PostgreSQL's &lt;code&gt;INSERT ON CONFLICT DO UPDATE&lt;/code&gt; instruction is built for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;account_balances&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_event_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;last_event_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_event_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;account_balances&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_event_id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_event_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- The WHERE clause prevents overwriting a more recent state with an older event&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;WHERE&lt;/code&gt; clause matters. Without it, if an old event arrives after a recent event (which can happen with multiple Kafka partitions), you overwrite a correct state with a stale one. With it, you only update if the incoming event is newer than what you already have.&lt;/p&gt;

&lt;p&gt;In practice, both approaches are complementary: the checkpoint avoids replaying thousands of events unnecessarily, and the idempotent upsert acts as a safety net for duplicates that slip through anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Outbox pattern — publishing events without data loss
&lt;/h2&gt;

&lt;p&gt;Suppose your system saves events in PostgreSQL and publishes them to Kafka for other services. If the process crashes after the PostgreSQL commit but before publishing to Kafka, the event is lost from the perspective of consumers. If you reverse the order, you might publish an event that the transaction will then rollback. It's impossible to do an atomic commit across two independent systems without a distributed coordinator.&lt;/p&gt;

&lt;p&gt;The Outbox pattern sidesteps the problem without two-phase commit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; In the same PostgreSQL transaction: save the event &lt;em&gt;and&lt;/em&gt; write it to an &lt;code&gt;outbox&lt;/code&gt; table&lt;/li&gt;
&lt;li&gt; A dedicated worker reads &lt;code&gt;outbox&lt;/code&gt; and publishes to Kafka&lt;/li&gt;
&lt;li&gt; Once successfully published, mark the row as sent
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;outbox&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt;   &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;      &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;published&lt;/span&gt;    &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;   &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;published_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;SaveEventAndOutbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sqlx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// 1. Save the event in the event store&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;saveEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// 2. Write to the outbox — same transaction, same commit or same rollback&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;`INSERT INTO outbox (event_type, payload) VALUES ($1, $2)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Separate worker — runs in the background&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OutboxWorker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ticker&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewTicker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Millisecond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;publishPending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"outbox publish failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&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="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OutboxWorker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;publishPending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;`SELECT id, event_type, payload FROM outbox
         WHERE published = FALSE
         ORDER BY created_at
         LIMIT 100`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"querying outbox: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UUID&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;eventType&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"scanning outbox row: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kafka&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"publishing event %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;`UPDATE outbox SET published = TRUE, published_at = NOW() WHERE id = $1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"marking event published %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&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;The worker may publish the same event twice if it crashes after the Kafka publish but before the &lt;code&gt;UPDATE outbox&lt;/code&gt;. That's at-least-once delivery, not exactly-once. Kafka consumers must therefore be idempotent — which the upsert approach described in the previous section guarantees.&lt;/p&gt;

&lt;p&gt;For high-volume systems, polling every 100ms can be replaced by &lt;a href="https://www.postgresql.org/docs/current/sql-notify.html" rel="noopener noreferrer"&gt;PostgreSQL LISTEN/NOTIFY&lt;/a&gt; to trigger the worker immediately after each outbox insertion, without waiting for the next tick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary — what protects what
&lt;/h2&gt;

&lt;p&gt;Problem&lt;/p&gt;

&lt;p&gt;Solution&lt;/p&gt;

&lt;p&gt;Command received twice (client retry)&lt;/p&gt;

&lt;p&gt;Idempotency key in &lt;code&gt;processed_commands&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Two concurrent commands on the same aggregate&lt;/p&gt;

&lt;p&gt;Optimistic locking (UNIQUE constraint on version)&lt;/p&gt;

&lt;p&gt;Event published twice by the broker&lt;/p&gt;

&lt;p&gt;Idempotent projection (checkpoint or upsert)&lt;/p&gt;

&lt;p&gt;Crash between save and publish&lt;/p&gt;

&lt;p&gt;Outbox pattern (atomicity via transaction)&lt;/p&gt;

&lt;p&gt;Each layer protects against a different type of duplicate. The idempotency key doesn't replace optimistic locking, and the outbox doesn't make the projection idempotent — these are orthogonal guarantees that each cover a distinct failure point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Idempotency in CQRS/Event Sourcing is not a detail you add after the fact — it's what separates a system that works in a demo from one that holds up in production. Retries happen. Processes crash. Kafka re-delivers messages. That's not paranoia, it's the normal behavior of any distributed system under load.&lt;/p&gt;

&lt;p&gt;The good news: these four patterns are well-known, well-supported by PostgreSQL, and assemble cleanly in Go. Once in place, they work silently — and that's exactly what you expect from them. The "duplicate payment" ticket that never comes in, that's the real success.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📄 Associated CLAUDE.md&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.web-developpeur.com/blog/claude-md/view.php?ctx=cqrs-event-sourcing-go" rel="noopener noreferrer"&gt;View&lt;/a&gt; • &lt;a href="https://www.web-developpeur.com/blog/claude-md/contexts/cqrs-event-sourcing-go.md" rel="noopener noreferrer"&gt;Download&lt;/a&gt; • &lt;a href="https://www.web-developpeur.com/blog/claude-md/" rel="noopener noreferrer"&gt;Catalogue&lt;/a&gt;&lt;/p&gt;

</description>
      <category>idempotence</category>
      <category>cqrs</category>
      <category>eventsourcing</category>
      <category>go</category>
    </item>
    <item>
      <title>Built tasuki — an AI CLI Orchestrator that Seamlessly Hands Off Between Tools</title>
      <dc:creator>kohei</dc:creator>
      <pubDate>Sun, 19 Apr 2026 08:49:32 +0000</pubDate>
      <link>https://forem.com/0xkohe/built-tasuki-an-ai-cli-orchestrator-that-seamlessly-hands-off-between-tools-519o</link>
      <guid>https://forem.com/0xkohe/built-tasuki-an-ai-cli-orchestrator-that-seamlessly-hands-off-between-tools-519o</guid>
      <description>&lt;p&gt;I built and open-sourced &lt;strong&gt;tasuki&lt;/strong&gt;, an AI CLI orchestrator that automatically rotates between &lt;strong&gt;Claude Code / Codex CLI / GitHub Copilot CLI&lt;/strong&gt; based on priority.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/0xkohe/tasuki" rel="noopener noreferrer"&gt;https://github.com/0xkohe/tasuki&lt;/a&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%2Fkcw64lvyid0gouvx7wpy.png" alt="Terminal screenshot showing tasuki orchestrating multiple AI CLIs and switching providers based on usage" width="800" height="367"&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;I’m the kind of person who prefers subscribing to multiple AI tools at $20–$30 each rather than committing to a single $100–$200 plan. I want to explore different tools and continuously feel the differences between models.&lt;/p&gt;

&lt;p&gt;However, doing this means hitting usage limits fairly quickly, which interrupts workflow and requires manual switching — something I found annoying.&lt;/p&gt;

&lt;p&gt;Each AI has its strengths — Codex for review, Claude for conversation, etc. But unless you’re extremely opinionated, there’s value in constantly rotating through tools and experiencing their evolution. If you stick to one, you may miss when another becomes better at something.&lt;/p&gt;

&lt;p&gt;The switching decision itself is trivial labor — so I wanted to automate it.&lt;br&gt;
That’s why I built tasuki.&lt;/p&gt;

&lt;p&gt;Instead of treating rate limits as a constraint, I treat them as an opportunity to move to another model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Intended Workflow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prioritize Claude Code and Codex CLI (5-hour windows)&lt;/strong&gt;&lt;br&gt;
Since their reset cycles are short, it’s more cost-efficient to exhaust them first.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Once both 5-hour windows are exhausted, switch to GitHub Copilot CLI&lt;/strong&gt;&lt;br&gt;
Copilot operates on a monthly quota, so it works well as a “bridge.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;When Claude / Codex reset, switch back automatically&lt;/strong&gt;&lt;br&gt;
Monthly quota should be preserved as much as possible.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Native UI stays intact&lt;/strong&gt;&lt;br&gt;
Each CLI runs inside a wrapped PTY, preserving the original interactive experience.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automatic switching based on usage&lt;/strong&gt;&lt;br&gt;
Adapters monitor CLI output streams and trigger a handoff when a threshold (default: 95%) is reached.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Context-aware handoff&lt;/strong&gt;&lt;br&gt;
Progress is written to &lt;code&gt;.tasuki/handoff.md&lt;/code&gt; and injected into the next provider.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Try It in 5 Minutes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/0xkohe/tasuki/cmd/tasuki@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  First Run
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;path/to/your/project
tasuki
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fo7aybiohnxwcpd9zlzc7.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%2Fo7aybiohnxwcpd9zlzc7.png" alt="Terminal output showing tasuki switching from Claude to Codex after hitting usage threshold" width="800" height="744"&gt;&lt;/a&gt;&lt;/p&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%2Fplufeba5hj9w73saebum.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%2Fplufeba5hj9w73saebum.png" alt="Terminal output showing tasuki continuing execution seamlessly on another provider" width="800" height="744"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Use everything. Keep exploring.&lt;br&gt;
tasuki reduces the friction of doing that.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/0xkohe/tasuki" rel="noopener noreferrer"&gt;https://github.com/0xkohe/tasuki&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Issues: &lt;a href="https://github.com/0xkohe/tasuki/issues" rel="noopener noreferrer"&gt;https://github.com/0xkohe/tasuki/issues&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>cli</category>
      <category>go</category>
      <category>programming</category>
    </item>
    <item>
      <title>OpenClaw is old? Run Hermes Agent in VS Code through ACP (Agent Client Protocol) now!</title>
      <dc:creator>Jun Han</dc:creator>
      <pubDate>Sun, 19 Apr 2026 08:49:21 +0000</pubDate>
      <link>https://forem.com/formulahendry/openclaw-is-old-run-hermes-agent-in-vs-code-through-acp-agent-client-protocol-now-2721</link>
      <guid>https://forem.com/formulahendry/openclaw-is-old-run-hermes-agent-in-vs-code-through-acp-agent-client-protocol-now-2721</guid>
      <description>&lt;p&gt;In recent weeks, the Hermes Agent becomes much populor in Agent world.&lt;/p&gt;

&lt;p&gt;Several users asked me about whether my &lt;a href="https://marketplace.visualstudio.com/items?itemName=formulahendry.acp-client" rel="noopener noreferrer"&gt;VS Code ACP Client extension&lt;/a&gt; could support Hermes Agent?&lt;/p&gt;

&lt;p&gt;So in this weekend, I have added support for Hermes Agent!&lt;/p&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%2F0inlrjh02mt0p6cqqqws.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%2F0inlrjh02mt0p6cqqqws.png" alt="Hermes Agent in VS Code" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Please note that, before you could run Hermes Agent in VS Code, install Hermes Agent via the &lt;a href="https://hermes-agent.nousresearch.com/docs/getting-started/quickstart" rel="noopener noreferrer"&gt;Hermes Quickstart&lt;/a&gt; (Linux/macOS/WSL2 only — Windows requires &lt;a href="https://learn.microsoft.com/en-us/windows/wsl/install" rel="noopener noreferrer"&gt;WSL2&lt;/a&gt;). Make sure &lt;code&gt;hermes&lt;/code&gt; is on your &lt;code&gt;PATH&lt;/code&gt; and launch VS Code from the same shell/venv. Configure credentials with &lt;code&gt;hermes model&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you are a Windows user, you need to connect to the Hermes Agent in WSL through the WSL VS Code extension:&lt;/p&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%2F5ucehioppt4779rg5a1b.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%2F5ucehioppt4779rg5a1b.png" alt="WSL VS Code extension" width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Actually, I've quietly developed three different ACP Clients that can suit different user groups!&lt;/p&gt;

&lt;p&gt;If you're a VS Code user, try the ​ACP Client extension:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/formulahendry/vscode-acp" rel="noopener noreferrer"&gt;https://github.com/formulahendry/vscode-acp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want a lightweight ACP Desktop interface on Windows/macOS/Linux, check out the cross-platform ​ACP UI:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/formulahendry/acp-ui" rel="noopener noreferrer"&gt;https://github.com/formulahendry/acp-ui&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to connect to an Agent from your phone via WeChat (iOS or Android), go for ​WeChat ACP:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/formulahendry/wechat-acp" rel="noopener noreferrer"&gt;https://github.com/formulahendry/wechat-acp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There’s something for everyone!&lt;/p&gt;

&lt;p&gt;Connect to any Agent, anytime, anywhere — effortlessly.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>showdev</category>
      <category>vscode</category>
    </item>
  </channel>
</rss>
