<?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: Asoseil</title>
    <description>The latest articles on Forem by Asoseil (@asoseil).</description>
    <link>https://forem.com/asoseil</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1934266%2F8856555c-29b5-4158-816f-7bf745016efa.png</url>
      <title>Forem: Asoseil</title>
      <link>https://forem.com/asoseil</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/asoseil"/>
    <language>en</language>
    <item>
      <title>From chaos to signal: Taming high-frequency OS events</title>
      <dc:creator>Asoseil</dc:creator>
      <pubDate>Tue, 20 Jan 2026 15:14:55 +0000</pubDate>
      <link>https://forem.com/asoseil/from-chaos-to-signal-taming-high-frequency-os-events-in-go-4p8k</link>
      <guid>https://forem.com/asoseil/from-chaos-to-signal-taming-high-frequency-os-events-in-go-4p8k</guid>
      <description>&lt;p&gt;If you have ever built a hot-reloader, a build tool, or a file sync agent in Go using raw &lt;code&gt;fsnotify&lt;/code&gt; or &lt;code&gt;syscall&lt;/code&gt; events, you have definitely encountered the "double-fire" problem.&lt;/p&gt;

&lt;p&gt;You save a file &lt;strong&gt;once&lt;/strong&gt;.&lt;br&gt;
Your terminal goes wild:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EVENT: "/src/main.go" [CHMOD]
EVENT: "/src/main.go" [RENAME]
EVENT: "/src/main.go" [WRITE]
EVENT: "/src/main.go" [WRITE]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your build triggers four times, your CPU spikes, logs scroll too fast to be read, and it's really frustrating.&lt;/p&gt;

&lt;p&gt;This isn't a bug in the code; it isn't even a bug in the library; it is fundamentally how operating systems work. But for a developer experience tool, "technically correct" feels broken.&lt;/p&gt;

&lt;p&gt;In this article, I will show you how I fixed this behaviour in &lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;sgtdi/fswatcher&lt;/a&gt; using &lt;strong&gt;Debouncing&lt;/strong&gt; and &lt;strong&gt;Smart filtering&lt;/strong&gt;, so you can build tools that feel instant and solid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Raw events vs. User intent
&lt;/h2&gt;

&lt;p&gt;Operating systems don't care about your "Save" intent. They report atomic file system operations, and when you hit &lt;code&gt;Ctrl+S&lt;/code&gt; in your IDE, the editor doesn't just open the file and write bytes, cause to ensure data safety, it often performs a "Safe Write" dance:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Create&lt;/strong&gt; a temporary file&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Write&lt;/strong&gt; content to the temporary file&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Delete&lt;/strong&gt; the original file&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Rename&lt;/strong&gt; the temp file to the original name&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Chmod&lt;/strong&gt; to restore permissions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To the OS, this looks like a flurry of creation, deletion, renaming, and attribute modification events, but to the final user, it's just one "Save".&lt;/p&gt;

&lt;p&gt;If your application reacts to every single one of these events, you are wasting resources. Worse, you might try to read the file in the nanoseconds between the "Delete" and the "Rename", causing your build tool to crash with a &lt;code&gt;file not found&lt;/code&gt; error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event Merging and Throttling
&lt;/h2&gt;

&lt;p&gt;I want my tools to be snappy and waiting for a "quiet period" (trailing edge debouncing) introduces lag. Instead, &lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;sgtdi/fswatcher&lt;/a&gt; uses a &lt;strong&gt;Leading edge&lt;/strong&gt; approach with event merging.&lt;/p&gt;

&lt;p&gt;The debouncer acts as a gatekeeper; it groups events by &lt;strong&gt;Path&lt;/strong&gt; and ensures that it can act immediately on the first sign of activity, suppressing the subsequent "echoes" caused by the OS.&lt;/p&gt;

&lt;h3&gt;
  
  
  The engine room
&lt;/h3&gt;

&lt;p&gt;Here is how I implemented this in &lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;sgtdi/fswatcher&lt;/a&gt;. The secret sauce is the &lt;code&gt;debouncer&lt;/code&gt; struct, which maintains a short-term memory of file activity.&lt;/p&gt;

&lt;p&gt;I use a map called &lt;code&gt;lastSeen&lt;/code&gt; to track the most recent event for every file path. This allows me to throttle events on a per-file basis—so a busy log file doesn't block the rest of the project. A &lt;code&gt;sync.Mutex&lt;/code&gt; is essential here because file system events arrive concurrently from multiple goroutines.&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;debouncer&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;lastSeen&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;WatchEvent&lt;/span&gt;
    &lt;span class="n"&gt;cooldown&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;Duration&lt;/span&gt;
    &lt;span class="n"&gt;mu&lt;/span&gt;       &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mutex&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logic for deciding whether to emit an event lives in &lt;code&gt;ShouldProcess&lt;/code&gt;. When a new event arrives, I lock the mutex to ensure safety.&lt;/p&gt;

&lt;p&gt;I check &lt;code&gt;lastSeen&lt;/code&gt; to see if this file has been active recently. If I find an existing entry and the new event arrived within the &lt;code&gt;cooldown&lt;/code&gt; window, I treat it as "noise" or an OS echo. Instead of emitting it, I &lt;strong&gt;merge&lt;/strong&gt; it into the previous event (combining flags like &lt;code&gt;Write&lt;/code&gt; + &lt;code&gt;Chmod&lt;/code&gt;) and suppress it.&lt;/p&gt;

&lt;p&gt;If the file is new or the cooldown has expired, I update the record and let the event pass through immediately. This ensures the user sees the result of their action instantly.&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;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;debouncer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ShouldProcess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="n"&gt;WatchEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WatchEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastSeen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exists&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;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cooldown&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;merged&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;mergeEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastSeen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;merged&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; 
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastSeen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This simple pattern is incredibly powerful cause It converts the chaotic stream of OS noise into a singular, meaningful signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using it in FSWatcher
&lt;/h3&gt;

&lt;p&gt;When you initialize the library, you just pass the &lt;code&gt;WithCooldown&lt;/code&gt; option and the library handles the concurrency complexity for you.&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="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fswatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;fswatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"./src"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;fswatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithCooldown&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="c"&gt;// The magic number&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, that flurry of 4-5 events becomes exactly &lt;strong&gt;one&lt;/strong&gt; &lt;code&gt;EventMod&lt;/code&gt; (Modify) event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ignoring the noise with smart filtering
&lt;/h2&gt;

&lt;p&gt;Debouncing handles &lt;em&gt;duplicate&lt;/em&gt; events, but what about &lt;em&gt;useless&lt;/em&gt; ones?&lt;/p&gt;

&lt;p&gt;If you are watching a project root, you rarely want to trigger a rebuild when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;.git/&lt;/code&gt; internal files change (index, HEAD updates).&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;node_modules&lt;/code&gt; gets updated by &lt;code&gt;npm install&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  OS junk files like &lt;code&gt;.DS_Store&lt;/code&gt; or &lt;code&gt;thumbs.db&lt;/code&gt; appear.&lt;/li&gt;
&lt;li&gt;  Your own build output directory (e.g., &lt;code&gt;dist/&lt;/code&gt; or &lt;code&gt;bin/&lt;/code&gt;) changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A naive watcher forces you to write a giant &lt;code&gt;if&lt;/code&gt; statement in your main event loop:&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="c"&gt;// The "Naive" Approach&lt;/span&gt;
&lt;span class="k"&gt;for&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;watcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Events&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;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Contains&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;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;".git"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;continue&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;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Contains&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;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"node_modules"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;continue&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;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasSuffix&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;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;".DS_Store"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// finally process...&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 tedious and error-prone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Platform-wware filtering
&lt;/h3&gt;

&lt;p&gt;To solve this elegantly, I need to respect platform conventions.&lt;/p&gt;

&lt;p&gt;First, let's handle &lt;strong&gt;System Artifacts&lt;/strong&gt;. Your OS leaves breadcrumbs everywhere—files like &lt;code&gt;.DS_Store&lt;/code&gt; on macOS, or &lt;code&gt;.swp&lt;/code&gt; files from text editors. By maintaining a list of these known prefixes and suffixes, I can identify and discard them before they ever reach your logic.&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="c"&gt;// platform_darwin.go&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;osPrefixes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"~"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&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;osSuffixes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;".DS_Store"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;".swp"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;isSystemFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&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;osPrefixes&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;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&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;true&lt;/span&gt; &lt;span class="p"&gt;}&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;_&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="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;osSuffixes&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;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasSuffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&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;true&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;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I handle &lt;strong&gt;Hidden Files&lt;/strong&gt;. In the Unix tradition, any file or directory starting with a dot &lt;code&gt;.&lt;/code&gt; is considered hidden. These often contain configuration or version control data (like &lt;code&gt;.git&lt;/code&gt;) that you typically want to ignore by default.&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="n"&gt;isHidden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&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;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The &lt;code&gt;fswatcher&lt;/code&gt; approach
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;sgtdi/fswatcher&lt;/a&gt; splits the responsibility to keep configuration clean:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;System Filters:&lt;/strong&gt; The library automatically handles the OS noise using the logic above. You don't need to configure it; it just works.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Regex Filters:&lt;/strong&gt; I leave the project-specific choices to you. Since generic hidden files (like &lt;code&gt;.git&lt;/code&gt;) are &lt;strong&gt;not&lt;/strong&gt; auto-excluded (you might want to watch them!), you use &lt;code&gt;WithExcRegex&lt;/code&gt; to define what matters for your specific project.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fswatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;fswatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c"&gt;// Exclude .git (hidden files aren't auto-ignored) and build artifacts&lt;/span&gt;
    &lt;span class="n"&gt;fswatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithExcRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;.git.*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;".*node_modules.*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;".*dist.*"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c"&gt;// Only explicitly include source files&lt;/span&gt;
    &lt;span class="n"&gt;fswatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithIncRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;.go$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;".*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;.html$"&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 gives you a clean stream of events that actually represent user changes, not system noise.&lt;/p&gt;

&lt;p&gt;Handling raw file system events is a trap. It leads to buggy, resource-intensive tools that frustrate users. By decoupling the &lt;strong&gt;OS event&lt;/strong&gt; (what happened technically) from the &lt;strong&gt;User intent&lt;/strong&gt; (what changed meaningfully), we can build tools that feel snappy, responsive, and polished.&lt;/p&gt;

&lt;p&gt;The combination of smart event throttling and robust regex filtering transforms a chaotic stream of system calls into a clean stream of actionable events.&lt;/p&gt;

&lt;p&gt;Check out the full implementation and try it in your next Go project: &lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;sgtdi/fswatcher&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>productivity</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>Stop Hitting CTRL+C: The way to hot reload Go apps</title>
      <dc:creator>Asoseil</dc:creator>
      <pubDate>Thu, 04 Dec 2025 08:13:48 +0000</pubDate>
      <link>https://forem.com/asoseil/stop-hitting-ctrlc-the-way-to-hot-reload-go-apps-747</link>
      <guid>https://forem.com/asoseil/stop-hitting-ctrlc-the-way-to-hot-reload-go-apps-747</guid>
      <description>&lt;p&gt;We need to talk about the "&lt;strong&gt;Micro-Interruption&lt;/strong&gt;" and what it actually does to your brain during an intense coding session. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You know the specific feeling I am talking about:&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;You are deep in the code mines, mentally holding a complex dependency graph in your head, and you finally implement the fix for a race condition. &lt;/li&gt;
&lt;li&gt;You hit &lt;strong&gt;Cmd+S to save&lt;/strong&gt;, and then the administrative ritual begins. &lt;/li&gt;
&lt;li&gt;You have to physically &lt;strong&gt;shift your focus from your code&lt;/strong&gt; editor to your terminal window, breaking your visual focus. &lt;/li&gt;
&lt;li&gt;You have to press &lt;strong&gt;Ctrl+C to send a kill signal to your running server&lt;/strong&gt; and wait a split second for the socket to close. &lt;/li&gt;
&lt;li&gt;You &lt;strong&gt;press the Up Arrow to recall your run command&lt;/strong&gt;, and finally, you hit Enter. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It seems like a trivial action that takes only a few seconds, but when you perform it hundreds of times a day, the cumulative effect is devastating. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It is not just the time lost; it is the destruction of your "&lt;strong&gt;Flow State&lt;/strong&gt;"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every single time you interact with the terminal to do administrative work, your brain drops a tiny piece of the complex logic you were holding. &lt;br&gt;
By the time the &lt;strong&gt;server/app restarts&lt;/strong&gt; and you look back at your code, you have to spend mental energy "&lt;strong&gt;reloading&lt;/strong&gt;" your own internal state, slowing down your momentum significantly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Make the tooling invisible
&lt;/h2&gt;

&lt;p&gt;To improve my daily development workflow, I built &lt;a href="https://github.com/sgtdi/vai" rel="noopener noreferrer"&gt;Vai&lt;/a&gt; cause I was exhausted from fighting my terminal and wanted a "&lt;strong&gt;save and see&lt;/strong&gt;" experience that felt native to the &lt;em&gt;Go ecosystem&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;It is a zero-dependency, &lt;strong&gt;zero-configuration CLI&lt;/strong&gt; tool that handles the restart cycle for you automatically, effectively removing the barrier between writing code and seeing the results of that code. It respects the Go philosophy of simplicity: a single binary that does one thing well. &lt;/p&gt;

&lt;p&gt;Unlike polling-based watchers that eat your battery with polling, it uses native OS filesystem events to react instantly with &lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;fswatcher&lt;/a&gt;, another low-level library that I recently published.&lt;/p&gt;
&lt;h2&gt;
  
  
  Zero Config, instant feedback
&lt;/h2&gt;

&lt;p&gt;The beauty of the tool lies in its absolute simplicity and lack of friction. If your standard workflow involves running your application with &lt;strong&gt;go run .&lt;/strong&gt;, you do not need to change your project structure, add a config file, or learn a new set of flags. &lt;/p&gt;

&lt;p&gt;Prefix your command with Vai, and instead of the manual cycle of stopping and starting, you transition to a "&lt;strong&gt;fire and forget&lt;/strong&gt;" mode. &lt;/p&gt;

&lt;p&gt;That is the extent of the onboarding process. From that moment on, Vai takes over the tedious process management loop. It &lt;strong&gt;monitors your .go files&lt;/strong&gt;, your &lt;strong&gt;go.mod&lt;/strong&gt;, and your &lt;strong&gt;go.sum&lt;/strong&gt; recursively with intelligent defaults, automatically ignoring hidden directories like .git or vendor folders.&lt;/p&gt;

&lt;p&gt;Here is what the immediate transition looks like in your daily workflow:&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;# ❌ The Old Way (The "Muscle Memory" Trap)&lt;/span&gt;
&lt;span class="c"&gt;# 1. Run the app&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;go run cmd/server/main.go
&lt;span class="c"&gt;# 2. Switch to editor, make changes, save&lt;/span&gt;
&lt;span class="c"&gt;# 3. Switch to terminal&lt;/span&gt;
&lt;span class="c"&gt;# 4. Hit CTRL+C to kill the process&lt;/span&gt;
&lt;span class="c"&gt;# 5. Hit Up Arrow, then Enter to restart&lt;/span&gt;
&lt;span class="c"&gt;# 6. Repeat 100x a day&lt;/span&gt;

&lt;span class="c"&gt;# ✅ The New Way (The "Flow State" Method)&lt;/span&gt;
&lt;span class="c"&gt;# 1. Run this once in the morning&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;vai go run cmd/server/main.go
&lt;span class="c"&gt;# 2. Never touch the terminal again&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are doing Test Driven Development, you can have Vai re-run your test suite every time you save a file, giving you immediate red/green feedback without lifting a finger.&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;# The standard way (boring and manual):&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; ./...

&lt;span class="c"&gt;# The TDD way (instant and automated):&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;vai go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the &lt;strong&gt;CLI mode&lt;/strong&gt;, we can construct a "one-liner" more advanced that acts as a complete development environment. There is support for command chaining, environment injection, and regex filtering all at once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vai &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"./services/billing"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--regex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;".*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.go$"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"PORT=8081,DB_HOST=localhost,DB_USER=postgres"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cmd&lt;/span&gt; &lt;span class="s2"&gt;"go fmt ./services/billing/..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cmd&lt;/span&gt; &lt;span class="s2"&gt;"go vet ./services/billing/..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cmd&lt;/span&gt; &lt;span class="s2"&gt;"go run ./services/billing/cmd/server"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Advanced workflow
&lt;/h2&gt;

&lt;p&gt;The CLI is powerful, but typing that command every day is tedious, and sharing it with a team is difficult. For complex, reproducible workflows, there is support for a &lt;strong&gt;customizable vai YAML file&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let's imagine a scenario where you are building a Go web application that serves HTML templates. You have a split requirement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend Code (.go)&lt;/strong&gt;: Needs to trigger a recompile and restart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend Templates (.html)&lt;/strong&gt;: Should also restart the server (to reload templates), but you don't need to run Go linters on HTML files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test Files (_test.go)&lt;/strong&gt;: Should never restart the server. Instead, they should run the test suite in parallel with the server to ensure you haven't broken anything.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;config:
  path: &lt;span class="nb"&gt;.&lt;/span&gt;
  logLevel: info

&lt;span class="nb"&gt;jobs&lt;/span&gt;:
  &lt;span class="c"&gt;# Triggers on Go files&lt;/span&gt;
  backend-dev:
    on:
      regex: 
        - &lt;span class="s2"&gt;".*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.go$"&lt;/span&gt;
        - &lt;span class="s2"&gt;"!.*_test.go$"&lt;/span&gt; &lt;span class="c"&gt;# Explicitly exclude tests&lt;/span&gt;
    series:
      - cmd: &lt;span class="s2"&gt;"golangci-lint run"&lt;/span&gt;
      - cmd: &lt;span class="s2"&gt;"go build -o ./tmp/app ."&lt;/span&gt;
      - cmd: &lt;span class="s2"&gt;"./tmp/app"&lt;/span&gt;

  &lt;span class="c"&gt;# Template Reloader&lt;/span&gt;
  frontend-reload:
    on:
      regex: 
        - &lt;span class="s2"&gt;".*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.html$"&lt;/span&gt;
    series:
      - cmd: &lt;span class="s2"&gt;"./tmp/app"&lt;/span&gt;

  &lt;span class="c"&gt;# Parallel test runner&lt;/span&gt;
  test-runner:
    on:
      regex: 
        - &lt;span class="s2"&gt;".*_test&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.go$"&lt;/span&gt;
    parallel:
      - cmd: &lt;span class="s2"&gt;"go test -v ./..."&lt;/span&gt;
      - cmd: &lt;span class="s2"&gt;"govulncheck ./..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether you are running a &lt;strong&gt;quick one-off script with the CLI&lt;/strong&gt; or &lt;strong&gt;orchestrating a complex microservices architecture&lt;/strong&gt; with a &lt;strong&gt;vai.yml&lt;/strong&gt;, the goal remains the same: remove the friction between your thought process and the running code.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/sgtdi/vai" rel="noopener noreferrer"&gt;Golang Vai&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;Fswatcher&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>development</category>
      <category>cli</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Linux, macOS, and Windows broke my code, so I built a cross-platform test strategy</title>
      <dc:creator>Asoseil</dc:creator>
      <pubDate>Fri, 21 Nov 2025 19:37:10 +0000</pubDate>
      <link>https://forem.com/asoseil/linux-macos-and-windows-broke-my-code-so-i-built-a-cross-platform-test-strategy-3633</link>
      <guid>https://forem.com/asoseil/linux-macos-and-windows-broke-my-code-so-i-built-a-cross-platform-test-strategy-3633</guid>
      <description>&lt;p&gt;Cross-platform &lt;strong&gt;filesystem events sound simple&lt;/strong&gt; in theory:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;make a file change, receive a notification&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In practice, three &lt;strong&gt;operating systems&lt;/strong&gt; interpret, buffer, group, and deliver those notifications in &lt;strong&gt;completely different ways&lt;/strong&gt;. The same action can trigger a single clean event on one platform, several intermediate ones on another, or a delayed burst somewhere else. Getting &lt;strong&gt;consistent behaviour across all&lt;/strong&gt; of them becomes surprisingly fragile, especially when &lt;strong&gt;concurrency and real-world workloads&lt;/strong&gt; enter the picture.&lt;/p&gt;

&lt;p&gt;I ran into all of this while building &lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;FSWatcher&lt;/a&gt;, a file-watching component that needed to behave consistently on every platform. Under the hood it uses &lt;strong&gt;FSEvents&lt;/strong&gt; on macOS, &lt;strong&gt;inotify&lt;/strong&gt; on Linux, and &lt;strong&gt;ReadDirectoryChangesW&lt;/strong&gt; on Windows, so it ended up exposing all the differences between the three systems at once. Once concurrency entered the picture, those differences became even more noticeable, and many assumptions I had initially made didn’t hold up.&lt;/p&gt;

&lt;p&gt;Correctness on one platform didn’t imply correctness on another, and correctness under light load didn’t tell me much about behaviour under stress. The &lt;strong&gt;test strategy&lt;/strong&gt; had to become more deliberate, &lt;strong&gt;more platform-aware&lt;/strong&gt;, and &lt;strong&gt;more concurrency-resistant&lt;/strong&gt;. The result is a pipeline that consistently exposes cross-platform issues early and keeps the watcher stable across all three environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-Platform behaviour is more different than you expect
&lt;/h2&gt;

&lt;p&gt;All three operating systems surface events in their own way. &lt;strong&gt;macOS&lt;/strong&gt; sometimes groups &lt;strong&gt;directory-level changes&lt;/strong&gt;; &lt;strong&gt;Linux&lt;/strong&gt; produces &lt;strong&gt;multiple events&lt;/strong&gt; for what conceptually feels like “one change”; &lt;strong&gt;Windows&lt;/strong&gt; often &lt;strong&gt;emits&lt;/strong&gt; cleaner, consolidated &lt;strong&gt;notifications&lt;/strong&gt;. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Any assumption about ordering, timing, or granularity breaks immediately.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, writing three files in sequence rarely produces events in that same order. Under heavy I/O, events can arrive seconds apart or all at once. This means &lt;strong&gt;tests can’t focus on sequences&lt;/strong&gt;, they have to focus on eventual consistency and presence of the expected state transitions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;FSWatcher&lt;/a&gt; also supports &lt;strong&gt;runtime path addition and removal&lt;/strong&gt;, and this affects the testing model: the suite needs to confirm that paths being watched dynamically start emitting events immediately, and removed paths truly stop. This pushes the tests into scenarios where the watcher has to respond to concurrent updates to its own internal structures.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI setup that doesn’t hide platform behaviours
&lt;/h2&gt;

&lt;p&gt;The current &lt;strong&gt;CI pipeline&lt;/strong&gt; runs the full suite on &lt;strong&gt;macOS (Intel and ARM), Linux, and Windows&lt;/strong&gt;. It’s intentionally configured not to stop early, because a Windows-only failure is still valuable even if Linux is green.&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="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macos-26&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macos-latest&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;windows-latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;Go race detector runs on macOS and Linux&lt;/strong&gt; and is responsible for catching most of the logic bugs that would otherwise appear as rare, intermittent failures. It has already flagged unsynchronised map writes, channel close races, and struct fields being read while another goroutine was updating them. These are the sorts of concurrency defects that aren’t visible in code review and rarely reproduce manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows builds skip -race&lt;/strong&gt; but still run the full suite, which is important because Windows has its own timing behaviours and handles resources differently than Unix-like systems.&lt;/p&gt;

&lt;p&gt;A periodic C*&lt;em&gt;odeQL analysis also runs to flag suspicious patterns&lt;/em&gt;* and unsafe syscall usage. Even though it isn’t concurrency-specific, it helps ensure that filesystem and OS interactions aren’t quietly failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing strategy cross-platform
&lt;/h2&gt;

&lt;p&gt;The tests had to change significantly to become stable. The &lt;strong&gt;early versions relied on fixed sleeps&lt;/strong&gt; and assumptions about order. Replacing those with &lt;strong&gt;event-driven constructs&lt;/strong&gt; made the suite consistent across runners.&lt;/p&gt;

&lt;p&gt;A readiness channel ensures that the watcher is fully started before operations begin:&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;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;readyChan&lt;/span&gt;&lt;span class="o"&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;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;After&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&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;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Watcher is not ready"&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;&lt;strong&gt;Event collection runs concurrently&lt;/strong&gt; in its own goroutine, which reflects how the watcher behaves in practice. The use of mutex-protected slices keeps the test deterministic without relying on sleep.&lt;/p&gt;

&lt;p&gt;One of the most important pieces is the &lt;strong&gt;randomized workload&lt;/strong&gt; generator:&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="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;performOperations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tempDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;performOperations performs a mix of file creation, modification, rename, directory creation, deletion, and nested deletion&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because the operations are randomized, each run produces different event patterns, and this forces the watcher to handle unpredictable sequences, essentially a built-in form of stress testing.&lt;/p&gt;

&lt;p&gt;The correctness check doesn’t compare order. It compares the presence of expected paths:&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;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;receivedEvents&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the tests ensure the watcher stops cleanly, channels close, and no goroutines outlive the test.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;FSWatcher&lt;/a&gt; also exposes statistics (processed, dropped, lost events). The &lt;strong&gt;tests validate that backpressure logic is correct&lt;/strong&gt; by filling channels intentionally and verifying that dropped events land in the right place and that lost events are accounted for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime path&lt;/strong&gt; addition and removal &lt;strong&gt;are tested&lt;/strong&gt; by creating temporary directories, watching multiple roots, and &lt;strong&gt;ensuring that removed paths do not emit events&lt;/strong&gt; even if files are changed afterward.&lt;/p&gt;

&lt;p&gt;All of this results in a suite that goes beyond basic unit testing: it &lt;strong&gt;simulates bursty workloads&lt;/strong&gt;, random event order, dynamic watch roots, and concurrent shutdown, exactly the kind of scenarios that real usage hits over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the current tests cover effectively
&lt;/h2&gt;

&lt;p&gt;Even &lt;strong&gt;without formal stress tools or property-based frameworks&lt;/strong&gt;, the suite already exercises several &lt;strong&gt;advanced behaviors&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;randomized file operations simulate realistic filesystem churn&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;concurrent event ingestion tests backpressure and channel behavior&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;dynamic path addition/removal stresses internal synchronization&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;multi-platform CI exposes OS-specific timing and ordering issues&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;shutdown tests verify that goroutines and resources clean up reliably&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;dropped/lost event tests validate overload behaviour&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;watchers handle thousands of events generated through random workloads&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This places &lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;FSWatcher&lt;/a&gt;’s test suite closer to a real integration environment rather than a traditional set of unit tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go further
&lt;/h2&gt;

&lt;p&gt;Since many stress-like behaviours are already present thanks to the randomized workload generator, the next improvements should be focused on areas not yet covered by the existing suite.&lt;/p&gt;

&lt;p&gt;A few upgrades would extend the reliability of the watcher in scenarios that aren’t currently exercised:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long-Running endurance tests&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Running the watcher for hours rather than seconds helps expose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;slow memory leaks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;handle/file-descriptor growth over time&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;goroutine buildup&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;timing drift under GC pressure&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This could run weekly in CI or manually before releases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fault injection with failpoints&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Injecting controlled failures into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;syscall returns&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;event dispatch paths&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;directory iteration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;queue overflows&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;would reveal whether &lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;FSWatcher&lt;/a&gt; recovers correctly from partial failures. Randomising delays inside goroutines would also help expose race windows that normal tests rarely hit.&lt;/p&gt;

&lt;p&gt;These two additions would meaningfully extend the testing scope without overlapping what the current suite already achieves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing concurrent, cross-platform code requires a strategy&lt;/strong&gt; that embraces inconsistency rather than fighting it. Different event models, different timing guarantees, and different behaviours under load mean the tests must be adaptive rather than prescriptive.&lt;/p&gt;

&lt;p&gt;The combination of multi-platform CI, race detection, randomized workloads, event-driven assertions, and strict cleanup has made &lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;FSWatcher&lt;/a&gt; stable across all three operating systems. With &lt;strong&gt;endurance testing and fault injection&lt;/strong&gt; added on top, the watcher would be able to withstand even more demanding production scenarios.&lt;/p&gt;

</description>
      <category>go</category>
      <category>programming</category>
      <category>testing</category>
      <category>development</category>
    </item>
    <item>
      <title>When a simple order update breaks Revolut Checkout workflow</title>
      <dc:creator>Asoseil</dc:creator>
      <pubDate>Fri, 14 Nov 2025 08:07:32 +0000</pubDate>
      <link>https://forem.com/asoseil/when-a-simple-order-update-breaks-revolut-checkout-workflow-ocf</link>
      <guid>https://forem.com/asoseil/when-a-simple-order-update-breaks-revolut-checkout-workflow-ocf</guid>
      <description>&lt;p&gt;During a recent payment gateway integration, i discovered a fundamental design flaw in the &lt;strong&gt;Revolut SDK&lt;/strong&gt; that breaks dynamic checkout flows and forces developers to implement complex workarounds. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Static state in a dynamic world
&lt;/h2&gt;

&lt;p&gt;On the web, &lt;strong&gt;RevolutCheckout&lt;/strong&gt; component requires creating a new order via API before rendering the checkout input form. This creates an unusual lifecycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend requests backend to create a Revolut order&lt;/li&gt;
&lt;li&gt;Backend calls Revolut API and receives an order token&lt;/li&gt;
&lt;li&gt;Frontend initializes RevolutCheckout with that token&lt;/li&gt;
&lt;li&gt;Payment component renders&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This flow has a critical limitation: the &lt;strong&gt;checkout component caches the initial order state&lt;/strong&gt; and &lt;strong&gt;never re-synchronizes it&lt;/strong&gt; with the backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  When things break
&lt;/h2&gt;

&lt;p&gt;In modern &lt;strong&gt;single-page checkout&lt;/strong&gt; experiences, users frequently modify their orders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adjusting quantities&lt;/li&gt;
&lt;li&gt;Applying discount codes&lt;/li&gt;
&lt;li&gt;Adding or removing items&lt;/li&gt;
&lt;li&gt;Updating shipping options&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each change requires &lt;strong&gt;updating the order&lt;/strong&gt; amount on Revolut's backend. While the API supports this via &lt;strong&gt;PATCH /api/orders/{order_id}&lt;/strong&gt;, the checkout component doesn't handle it.&lt;/p&gt;

&lt;p&gt;When you update an order after the checkout component has mounted, attempting to complete the payment fails with a generic error:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Transaction failed. Please contact the merchant to resolve this problem."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Reproduction
&lt;/h2&gt;

&lt;p&gt;Here's a minimal example that demonstrates the issue:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create an Order&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;curl &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'https://sandbox-merchant.revolut.com/api/orders'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Revolut-Api-Version: 2025-10-16'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer &amp;lt;key&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--data-raw&lt;/span&gt; &lt;span class="s1"&gt;'{"amount": 500, "currency": "EUR"}'&lt;/span&gt;

Response:
json&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"540d94a9-b356-a52f-8870-dabaac383a1b"&lt;/span&gt;,
  &lt;span class="s2"&gt;"token"&lt;/span&gt;: &lt;span class="s2"&gt;"b140372e-cb3b-47f8-8c7e-b22090ea4ac2"&lt;/span&gt;,
  &lt;span class="s2"&gt;"type"&lt;/span&gt;: &lt;span class="s2"&gt;"payment"&lt;/span&gt;,
  &lt;span class="s2"&gt;"state"&lt;/span&gt;: &lt;span class="s2"&gt;"pending"&lt;/span&gt;,
  &lt;span class="s2"&gt;"created_at"&lt;/span&gt;: &lt;span class="s2"&gt;"2025-11-07T06:41:45.820229Z"&lt;/span&gt;,
  &lt;span class="s2"&gt;"updated_at"&lt;/span&gt;: &lt;span class="s2"&gt;"2025-11-07T06:41:45.820229Z"&lt;/span&gt;,
  &lt;span class="s2"&gt;"amount"&lt;/span&gt;: 500,
  &lt;span class="s2"&gt;"currency"&lt;/span&gt;: &lt;span class="s2"&gt;"EUR"&lt;/span&gt;,
  &lt;span class="s2"&gt;"outstanding_amount"&lt;/span&gt;: 500
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Update the Order (after RevolutCheckout is mount)&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;curl &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; PATCH &lt;span class="s1"&gt;'https://sandbox-merchant.revolut.com/api/orders/{order_id}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Revolut-Api-Version: 2025-10-16'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer &amp;lt;key&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--data-raw&lt;/span&gt; &lt;span class="s1"&gt;'{"amount": 1000, "currency": "EUR"}'&lt;/span&gt;

Response:
json&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"540d94a9-b356-a52f-8870-dabaac383a1b"&lt;/span&gt;,
  &lt;span class="s2"&gt;"token"&lt;/span&gt;: &lt;span class="s2"&gt;"b140372e-cb3b-47f8-8c7e-b22090ea4ac2"&lt;/span&gt;,
  &lt;span class="s2"&gt;"type"&lt;/span&gt;: &lt;span class="s2"&gt;"payment"&lt;/span&gt;,
  &lt;span class="s2"&gt;"state"&lt;/span&gt;: &lt;span class="s2"&gt;"pending"&lt;/span&gt;,
  &lt;span class="s2"&gt;"created_at"&lt;/span&gt;: &lt;span class="s2"&gt;"2025-11-07T06:41:45.820229Z"&lt;/span&gt;,
  &lt;span class="s2"&gt;"updated_at"&lt;/span&gt;: &lt;span class="s2"&gt;"2025-11-07T06:43:21.785741Z"&lt;/span&gt;,
  &lt;span class="s2"&gt;"amount"&lt;/span&gt;: 1000,
  &lt;span class="s2"&gt;"currency"&lt;/span&gt;: &lt;span class="s2"&gt;"EUR"&lt;/span&gt;,
  &lt;span class="s2"&gt;"outstanding_amount"&lt;/span&gt;: 1000
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;order ID and token remain valid&lt;/strong&gt;, but the &lt;strong&gt;checkout component will fail&lt;/strong&gt; when attempting to complete the payment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root cause analysis
&lt;/h2&gt;

&lt;p&gt;The issue stems from a state synchronization mismatch between client and server:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RevolutCheckout caches&lt;/strong&gt; the initial order state (including &lt;strong&gt;amount&lt;/strong&gt;) when initialized, and the cached state is never refreshed, even when the order is updated via API.&lt;/p&gt;

&lt;p&gt;When the user submits a payment, the &lt;strong&gt;component sends the cached amount&lt;/strong&gt;, Revolut's backend detects the &lt;strong&gt;mismatch with the current amount&lt;/strong&gt; and &lt;strong&gt;rejects the payment&lt;/strong&gt; with a generic error message that provides no clarity about the actual issue.&lt;/p&gt;

&lt;p&gt;This is a classic case of assuming &lt;strong&gt;immutability on the client side&lt;/strong&gt; while allowing mutability on the server side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workaround
&lt;/h2&gt;

&lt;p&gt;The only solution is to &lt;strong&gt;remount the checkout component whenever the order changes&lt;/strong&gt;. Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateOrder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Update order via API&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateRevolutOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Completely reinitialize the checkout components&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;initializeCardField&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;initializeRevolutPay&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error updating Revolut order:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This workaround has significant drawbacks and requires remounting if you use both &lt;strong&gt;RevolutCheckout&lt;/strong&gt; for the &lt;strong&gt;card field&lt;/strong&gt; and &lt;strong&gt;Revolut Pay&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Poor user experience&lt;/strong&gt;&lt;br&gt;
Every order &lt;strong&gt;update triggers a visible remount cycle&lt;/strong&gt;, causing the checkout form to disappear and reappear. This creates &lt;strong&gt;flickering UI&lt;/strong&gt; and resets any partial input.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Technical Overhead&lt;/strong&gt;&lt;br&gt;
Backend logs become cluttered with abandoned order records while additional API calls are triggered for every modification, requiring complex state management to handle remounting logic and creating potential race conditions between user interactions and component lifecycle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Silent failures&lt;/strong&gt;&lt;br&gt;
Without discovering this through extensive testing, payments fail with no indication of the actual problem, and users see generic error messages that incorrectly suggest contacting the merchant, eroding trust in the checkout process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Maintenance burden&lt;/strong&gt;&lt;br&gt;
Developers must build and maintain logic that should be abstracted within the SDK itself.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What should happen
&lt;/h2&gt;

&lt;p&gt;A robust checkout SDK should handle order updates in one of two ways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Invalidate on update&lt;/strong&gt;&lt;br&gt;
When an order is updated via API, &lt;strong&gt;invalidate the existing token&lt;/strong&gt; and require a new checkout initialization. Make this explicit in the API response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2: Auto-refresh state&lt;/strong&gt;&lt;br&gt;
Before submitting payment, **automatically fetch the latest order state **from the backend to ensure synchronization. This could happen transparently within the SDK.&lt;/p&gt;

&lt;p&gt;Either approach would eliminate the need for workarounds and provide clear, predictable behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other payment gateways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stripe's&lt;/strong&gt; approach, for comparison, separates the concepts of &lt;strong&gt;PaymentIntent&lt;/strong&gt; (&lt;strong&gt;immutable&lt;/strong&gt;) and &lt;strong&gt;amount confirmation&lt;/strong&gt; (&lt;strong&gt;mutable&lt;/strong&gt;). You can update amounts before confirmation without breaking the payment flow, and the Elements components handle state synchronization internally.&lt;/p&gt;

&lt;p&gt;This &lt;strong&gt;isn't an edge case—order updates&lt;/strong&gt; are a standard feature of e-commerce checkouts. Dynamic pricing, discount codes, quantity adjustments, and real-time calculations are expected in modern applications.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;RevolutCheckout&lt;/strong&gt; inability to handle these updates gracefully represents a &lt;strong&gt;fundamental architectural flaw&lt;/strong&gt;. It shifts the complexity that should be handled by the payment gateway onto every merchant integration.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For developers considering Revolut: Be aware that implementing dynamic checkout flows requires building significant workaround logic. Test order update scenarios thoroughly before going to production.&lt;/p&gt;

&lt;p&gt;For the Revolut team: This issue has been reported with reproducible test cases. A fix would significantly improve developer experience and bring the SDK in line with modern payment gateway expectations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://github.com/revolut-engineering/revolut-checkout/issues/101" rel="noopener noreferrer"&gt;Github revolut-checkout issue&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>development</category>
      <category>webdev</category>
      <category>revolut</category>
    </item>
    <item>
      <title>How macOS, Linux, and Windows detect file changes (and why it's hard to catch them)</title>
      <dc:creator>Asoseil</dc:creator>
      <pubDate>Wed, 12 Nov 2025 10:10:36 +0000</pubDate>
      <link>https://forem.com/asoseil/how-macos-linux-and-windows-detect-file-changes-and-why-it-isnt-easy-194m</link>
      <guid>https://forem.com/asoseil/how-macos-linux-and-windows-detect-file-changes-and-why-it-isnt-easy-194m</guid>
      <description>&lt;p&gt;File watching seems simple on the surface: "&lt;em&gt;tell me when a file changes&lt;/em&gt;" But the reality is &lt;strong&gt;three completely different APIs with three fundamentally different philosophies&lt;/strong&gt; about how computers should monitor file systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three main approaches
&lt;/h2&gt;

&lt;p&gt;I recently spent weeks building &lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;FSWatcher&lt;/a&gt;, a cross-platform file watcher in Go, and the journey taught me that understanding file watching means understanding how each operating system approaches the problems differently. &lt;/p&gt;

&lt;h3&gt;
  
  
  🍎 macOS: FSEvents (Directory-first)
&lt;/h3&gt;

&lt;p&gt;Apple's philosophy is to monitor directory trees, rather than individual files.&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="c"&gt;// You say: "watch /Users/me/fswatcher"&lt;/span&gt;
&lt;span class="c"&gt;// macOS says: "OK, I'll tell you when ANYTHING in that tree changes"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Low CPU usage&lt;/li&gt;
&lt;li&gt;Efficient for large directories&lt;/li&gt;
&lt;li&gt;Event-driven (no polling)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gives you directory-level info, not file-level&lt;/li&gt;
&lt;li&gt;Can flood you with redundant events&lt;/li&gt;
&lt;li&gt;You must filter what you actually care about&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example event&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;//Event: /Users/me/fswatcher/src changed&lt;/span&gt;
&lt;span class="c"&gt;// You have to figure out WHAT changed in /src&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🐧 Linux: inotify (File-First)
&lt;/h3&gt;

&lt;p&gt;Linux's philosophy: granular control over specific files and directories.&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="c"&gt;// You say: "watch /home/me/fswatcher/main.go"&lt;/span&gt;
&lt;span class="c"&gt;// Linux says: "OK, I'll tell you exactly what happens to that file"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Precise, file-level events&lt;/li&gt;
&lt;li&gt;You know exactly what changed&lt;/li&gt;
&lt;li&gt;Low-level control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each watch = one file descriptor (limited resource)&lt;/li&gt;
&lt;li&gt;Easy to hit system limits on large projects&lt;/li&gt;
&lt;li&gt;More prone to event flooding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example event&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Event: /home/me/fswatcher/main.go MODIFIED&lt;/span&gt;
&lt;span class="c"&gt;// Event: /home/me/fswatcher/main.go ATTRIBUTES_CHANGED&lt;/span&gt;
&lt;span class="c"&gt;// Event: /home/me/fswatcher/main.go CLOSE_WRITE&lt;/span&gt;
&lt;span class="c"&gt;// Same file save = 3 events&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🪟 Windows: ReadDirectoryChangesW (Async-first)
&lt;/h3&gt;

&lt;p&gt;Windows philosophy: asynchronous I/O with overlapping operations.&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="c"&gt;// You say: "watch C:\project and give me async notifications"&lt;/span&gt;
&lt;span class="c"&gt;// Windows says: "I'll buffer changes and notify you asynchronously"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fast asynchronous I/O&lt;/li&gt;
&lt;li&gt;Efficient buffering&lt;/li&gt;
&lt;li&gt;Scales well&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requires careful buffer management&lt;/li&gt;
&lt;li&gt;Can lose events if the buffer overflows&lt;/li&gt;
&lt;li&gt;Complex synchronization needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example event&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Event: C:\project\main.go MODIFIED (buffered)&lt;/span&gt;
&lt;span class="c"&gt;// Event: C:\project\test.go CREATED (buffered)&lt;/span&gt;
&lt;span class="c"&gt;// Events may be batched by Windows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The real challenges
&lt;/h2&gt;

&lt;p&gt;Challenge 1: &lt;strong&gt;Event Inconsistency&lt;/strong&gt;&lt;br&gt;
Same action (save a file) → different events per OS:&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="c"&gt;// macOS&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="n"&gt;project&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Linux  &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;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;MODIFIED&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;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;ATTRIB&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;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;CLOSE_WRITE&lt;/span&gt;

&lt;span class="c"&gt;// Windows&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;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;MODIFIED&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Challenge 2: &lt;strong&gt;Editor Spam&lt;/strong&gt;&lt;br&gt;
Modern editors (VSCode, GoLand) don't just save once:&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="m"&gt;1.&lt;/span&gt; &lt;span class="n"&gt;Create&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="m"&gt;2.&lt;/span&gt; &lt;span class="n"&gt;Write&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;
&lt;span class="m"&gt;3.&lt;/span&gt; &lt;span class="n"&gt;Delete&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt;
&lt;span class="m"&gt;4.&lt;/span&gt; &lt;span class="n"&gt;Rename&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt;
&lt;span class="m"&gt;5.&lt;/span&gt; &lt;span class="n"&gt;Update&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;
&lt;span class="m"&gt;6.&lt;/span&gt; &lt;span class="n"&gt;Flush&lt;/span&gt; &lt;span class="n"&gt;buffers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's 6+ events for ONE save operation!&lt;/p&gt;

&lt;p&gt;Challenge 3: &lt;strong&gt;Bulk Operations&lt;/strong&gt;&lt;br&gt;
When you run git checkout, thousands of files change instantly:&lt;br&gt;
bashgit checkout main&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="err"&gt;#&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;000&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;000&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="err"&gt;~&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your watcher must handle this flood without crashing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A unified pipeline to solve inconsistency
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;FSWatcher&lt;/a&gt;, I built a pipeline that normalizes all these differences:&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="err"&gt;┌─────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="n"&gt;OS&lt;/span&gt; &lt;span class="n"&gt;Events&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;specific&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;└──────┬──────┘&lt;/span&gt;
       &lt;span class="err"&gt;│&lt;/span&gt;
       &lt;span class="err"&gt;▼&lt;/span&gt;
&lt;span class="err"&gt;┌─────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="n"&gt;Normalize&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;consistent&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;└──────┬──────┘&lt;/span&gt;
       &lt;span class="err"&gt;│&lt;/span&gt;
       &lt;span class="err"&gt;▼&lt;/span&gt;
&lt;span class="err"&gt;┌─────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="n"&gt;Debounce&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merge&lt;/span&gt; &lt;span class="n"&gt;rapid&lt;/span&gt; &lt;span class="n"&gt;duplicates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;└──────┬──────┘&lt;/span&gt;
       &lt;span class="err"&gt;│&lt;/span&gt;
       &lt;span class="err"&gt;▼&lt;/span&gt;
&lt;span class="err"&gt;┌─────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="n"&gt;Batch&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="n"&gt;related&lt;/span&gt; &lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;└──────┬──────┘&lt;/span&gt;
       &lt;span class="err"&gt;│&lt;/span&gt;
       &lt;span class="err"&gt;▼&lt;/span&gt;
&lt;span class="err"&gt;┌─────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="n"&gt;Filter&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regex&lt;/span&gt; &lt;span class="n"&gt;include&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;└──────┬──────┘&lt;/span&gt;
       &lt;span class="err"&gt;│&lt;/span&gt;
       &lt;span class="err"&gt;▼&lt;/span&gt;
&lt;span class="err"&gt;┌─────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="n"&gt;Clean&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt; &lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;└─────────────┘&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Debouncing in action&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Without debouncing:&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;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;00&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;00.100&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;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;00&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;00.150&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;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;00&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;00.200&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;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;00&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;00.250&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;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;00&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;00.300&lt;/span&gt;

&lt;span class="c"&gt;// With debouncing (300ms window):&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;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;00&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;00.300&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Batching in action&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Without batching:&lt;/span&gt;
&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="n"&gt;separate&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="n"&gt;notifications&lt;/span&gt;

&lt;span class="c"&gt;// With batching:&lt;/span&gt;
&lt;span class="n"&gt;Batch&lt;/span&gt;&lt;span class="o"&gt;:&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="m"&gt;10&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real example
&lt;/h2&gt;

&lt;p&gt;Here's a hot-reload system using FSWatcher:&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;go&lt;/span&gt; &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;sgtdi&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;fswatcher&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;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/sgtdi/fswatcher"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Only watch .go files and ignore .go files under test dir&lt;/span&gt;
    &lt;span class="n"&gt;fsw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fswatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;fswatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithIncRegex&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;`\.go$`&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="n"&gt;fswatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithExcRegex&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;`test/.*\.go$`&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;)&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;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;fsw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Watch&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Starting.."&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;e&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;fsw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&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;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;())&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;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Changed.."&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also wrote a detailed article on Medium about the implementation journey and lessons learned: &lt;a href="https://medium.com/@asoseil/the-file-system-rabbit-hole-building-fswatcher-in-go-eb890fde01b3" rel="noopener noreferrer"&gt;Read the full story&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;FSWatcher&lt;/a&gt;&lt;br&gt;
&lt;a href="https://developer.apple.com/documentation/coreservices/file_system_events" rel="noopener noreferrer"&gt;Apple FSEvents Documentation&lt;/a&gt;&lt;br&gt;
&lt;a href="https://man7.org/linux/man-pages/man7/inotify.7.html" rel="noopener noreferrer"&gt;Linux inotify man page&lt;/a&gt;&lt;br&gt;
&lt;a href="https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw" rel="noopener noreferrer"&gt;Windows ReadDirectoryChangesW&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>testing</category>
      <category>development</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building a cross-platform File Watcher in Go: What I learned from scratch</title>
      <dc:creator>Asoseil</dc:creator>
      <pubDate>Sun, 09 Nov 2025 11:22:16 +0000</pubDate>
      <link>https://forem.com/asoseil/building-a-cross-platform-file-watcher-in-go-what-i-learned-from-scratch-1dbj</link>
      <guid>https://forem.com/asoseil/building-a-cross-platform-file-watcher-in-go-what-i-learned-from-scratch-1dbj</guid>
      <description>&lt;p&gt;Ever wondered how hot-reload tools actually detect when you save a file? I did too. So instead of just using an existing library, I spent weeks building one from scratch.&lt;/p&gt;

&lt;p&gt;This is the story of &lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;FSWatcher&lt;/a&gt;, a cross-platform &lt;strong&gt;file system watcher&lt;/strong&gt; for &lt;strong&gt;Go&lt;/strong&gt; that taught me more about operating systems and concurrency than any tutorial ever could.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem that started everything
&lt;/h2&gt;

&lt;p&gt;A while ago, I had built a simple &lt;strong&gt;hot-reload tool for a Go project&lt;/strong&gt;, it worked great using fsnotify, but I had no idea what was happening under the hood. When I save a file in VSCode, how does my computer actually know that something changed?&lt;/p&gt;

&lt;p&gt;That curiosity led me down a rabbit hole that became &lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;FSWatcher&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes file watching so complex?
&lt;/h2&gt;

&lt;p&gt;Here's the thing: each operating system handles file monitoring completely differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS: FSEvents&lt;/strong&gt;&lt;br&gt;
On macOS, you don't watch individual files, you watch entire directory trees. Apple's FSEvents API is efficient and event-driven, but it floods you with information you don't always need.&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="c"&gt;// macOS watches directories, not files&lt;/span&gt;
&lt;span class="c"&gt;// On file change return an event like: &lt;/span&gt;
&lt;span class="s"&gt;"something changed in /Users/you/project/file.txt"&lt;/span&gt;
&lt;span class="c"&gt;// But return also events about temporary files: &lt;/span&gt;
&lt;span class="s"&gt;"something changed in /Users/you/project/file.txt~"&lt;/span&gt;
&lt;span class="c"&gt;// And also many events for each action on the file, like:&lt;/span&gt;
 &lt;span class="s"&gt;"something changed in /Users/you/project/file.txt~"&lt;/span&gt; &lt;span class="n"&gt;CHMOD&lt;/span&gt;
 &lt;span class="s"&gt;"something changed in /Users/you/project/file.txt~"&lt;/span&gt; &lt;span class="n"&gt;WRITE&lt;/span&gt;
&lt;span class="c"&gt;//and so on&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Linux: inotify&lt;/strong&gt;&lt;br&gt;
Linux's inotify is more granular; you can watch specific files, but it's also more fragile. Each watch consumes a file descriptor, and on a large project, you can easily hit system limits.&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="c"&gt;// Linux is precise but limited&lt;/span&gt;
&lt;span class="c"&gt;// Each file watch = one file descriptor&lt;/span&gt;
&lt;span class="c"&gt;// Large projects = potential resource exhaustion due to max limits&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windows: ReadDirectoryChangesW&lt;/strong&gt;&lt;br&gt;
Windows uses asynchronous I/O with overlapping reads. It's fast, but requires careful coordination of buffers and synchronization to avoid losing events.&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="c"&gt;// Windows is async-first&lt;/span&gt;
&lt;span class="c"&gt;// Requires buffer management and careful synchronization&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The architecture challenge
&lt;/h2&gt;

&lt;p&gt;The first version of FSWatcher was a mess. I had goroutines everywhere, different code paths for each OS, and events that didn't match across platforms.&lt;/p&gt;

&lt;p&gt;The breakthrough came when I realized I needed a unified pipeline:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;OS Events → Normalization → Debouncing → Batching → Clean Events&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Normalization&lt;/strong&gt;&lt;br&gt;
Convert platform-specific events into a consistent format&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debouncing&lt;/strong&gt;&lt;br&gt;
When you save a file in VSCode or another editor, it might trigger 3-5 events in quick succession. Debouncing merges them into one:&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="c"&gt;// Instead of:&lt;/span&gt;
&lt;span class="c"&gt;// Event: file.go changed&lt;/span&gt;
&lt;span class="c"&gt;// Event: file.go changed&lt;/span&gt;
&lt;span class="c"&gt;// Event: file.go changed&lt;/span&gt;

&lt;span class="c"&gt;// You get a single event:&lt;/span&gt;
&lt;span class="c"&gt;// Event: file.go changed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Batching&lt;/strong&gt;&lt;br&gt;
When there are many changes with different operations like WRITE, CREATED, CHMOD, and so on, the batch is released as a single WatchEvent.&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="c"&gt;// Instead of many individual events&lt;/span&gt;
&lt;span class="c"&gt;// You get a single event with the multiple operations &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real problems, real solutions
&lt;/h2&gt;

&lt;p&gt;None of these features was planned, they all came from actual pain points:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: VSCode saves trigger 5 events per file&lt;br&gt;
&lt;em&gt;Solution&lt;/em&gt;: Debouncing with configurable cooldown&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: npm install creates 10,000+ events&lt;br&gt;
&lt;em&gt;Solution&lt;/em&gt;: Batching system to group related changes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: I don't care about .git or node_modules changes&lt;br&gt;
&lt;em&gt;Solution&lt;/em&gt;: Regex filtering (include/exclude patterns)&lt;/p&gt;
&lt;h2&gt;
  
  
  Testing across platforms
&lt;/h2&gt;

&lt;p&gt;After many tests, I also realized that manual testing wasn't sustainable. So i built a GitHub Actions pipeline that runs automated tests on macOS, Linux, and Windows on every commit.&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="na"&gt;yamlstrategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;macos-latest&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;windows-latest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This caught race conditions and edge cases I would have never found manually or with a Docker emulation of the os&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use FSWatcher
&lt;/h2&gt;

&lt;p&gt;After all that complexity, the API is intentionally simple:&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;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/sgtdi/fswatcher"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Create watcher for current directory&lt;/span&gt;
    &lt;span class="n"&gt;fsw&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;fswatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&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="nb"&gt;panic&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;// Start watching&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;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;fsw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Watch&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="c"&gt;// Handle events&lt;/span&gt;
    &lt;span class="k"&gt;for&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;fsw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&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;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"File changed: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&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;Path&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No dependencies, no complex configuration. Just clean events. Obviously, there are a lot of additional options to personalize the workflow, but by default, it can be used with very few lines of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Simplicity is hard&lt;/strong&gt;: making something simple to use often means handling massive complexity internally&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test everything&lt;/strong&gt;: Cross-platform code needs automated testing across all platforms&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Real problems &amp;gt; Planned features&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every feature in &lt;a href="https://github.com/sgtdi/fswatcher" rel="noopener noreferrer"&gt;FSWatcher&lt;/a&gt; came from actual friction. Go's concurrency model is powerful, and Goroutines made managing multiple OS backends possible&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;FSWatcher is open source and ready to use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/sgtdi/fswatcher
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub Repository: &lt;a href="//github.com/sgtdi/fswatcher"&gt;github.com/sgtdi/fswatcher&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also wrote a deep dive article on Medium explaining the OS-level details and implementation challenges: &lt;a href="https://medium.com/@asoseil/the-file-system-rabbit-hole-building-fswatcher-in-go-eb890fde01b3" rel="noopener noreferrer"&gt;Read the full story&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Right now I'm actively working on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performance benchmarks vs other watchers&lt;/li&gt;
&lt;li&gt;More examples and use cases&lt;/li&gt;
&lt;li&gt;Additional filtering options&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you find it useful, &lt;strong&gt;star the repo&lt;/strong&gt;! And if you find bugs or have ideas, issues and &lt;strong&gt;PRs are welcome&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>go</category>
      <category>opensource</category>
      <category>tutorial</category>
      <category>github</category>
    </item>
  </channel>
</rss>
