<?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: Matías Denda</title>
    <description>The latest articles on Forem by Matías Denda (@mdenda).</description>
    <link>https://forem.com/mdenda</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%2F3601966%2F6aebccb4-b6ac-40c9-98c1-0eb5fa97d314.png</url>
      <title>Forem: Matías Denda</title>
      <link>https://forem.com/mdenda</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mdenda"/>
    <language>en</language>
    <item>
      <title>What cave diving taught me about distributed systems</title>
      <dc:creator>Matías Denda</dc:creator>
      <pubDate>Thu, 23 Apr 2026 16:32:38 +0000</pubDate>
      <link>https://forem.com/mdenda/what-cave-diving-taught-me-about-distributed-systems-2a83</link>
      <guid>https://forem.com/mdenda/what-cave-diving-taught-me-about-distributed-systems-2a83</guid>
      <description>&lt;h2&gt;
  
  
  What cave diving taught me about distributed systems
&lt;/h2&gt;

&lt;p&gt;I've been building backend systems for 14 years. I've also spent a decent chunk of the last decade underwater, mostly in caves.&lt;/p&gt;

&lt;p&gt;At some point I stopped being surprised by how often the two worlds rhyme. The deeper you go into either, the more you notice the same ideas showing up in different costumes. Here are a few that stuck with me.&lt;/p&gt;

&lt;h2&gt;
  
  
  You plan the dive, then you dive the plan
&lt;/h2&gt;

&lt;p&gt;In open water, if something goes wrong, you go up. That's it. The surface is always there, a few kicks away, a guaranteed exit.&lt;/p&gt;

&lt;p&gt;In a cave, there is no "up". There's a ceiling, and between you and air there's sometimes hundreds of meters of rock and a specific path you came in through. If something goes wrong at the end of a one-hour penetration, the solution is still one hour of swimming away — and you're the one who has to swim it, with whatever gas, light, and composure you have left.&lt;/p&gt;

&lt;p&gt;So technical divers plan &lt;em&gt;everything&lt;/em&gt; before getting in the water. Gas volumes for every phase, with reserves for the worst case and the worst case after that. Turn points. Decompression schedules. Equipment failures and who does what when they happen. Team positions, signals, lost-diver procedures. Murphy's law isn't a joke in this context — it's a design input.&lt;/p&gt;

&lt;p&gt;The rule is: &lt;em&gt;plan the dive, dive the plan.&lt;/em&gt; You don't improvise &lt;br&gt;
underwater. You execute what you already decided on land, when your &lt;br&gt;
brain had oxygen and no time pressure.&lt;/p&gt;

&lt;p&gt;Software has the same trap, and most teams fall into it. "We'll figure it out in production" is the engineering equivalent of "we'll figure it out at 80 meters." Sometimes you get lucky. Often you don't.&lt;/p&gt;

&lt;p&gt;The work that matters — capacity planning, failure mode analysis, &lt;br&gt;
runbooks, rollback procedures, on-call rotations, dependency mapping — happens &lt;em&gt;before&lt;/em&gt; the system is under load. Before the incident. Before anyone is stressed. Because the incident is not the time to start thinking. It's time to execute what you already thought through.&lt;/p&gt;

&lt;p&gt;And just like diving, the planning doesn't eliminate failure. It just makes sure that when failure shows up, you've already met it on paper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failures cascade. Plan for the second failure, not the first.
&lt;/h2&gt;

&lt;p&gt;The thing that kills divers isn't usually the first problem. It's the panic reaction to the first problem that causes the second one — and the second one is the one you weren't ready for.&lt;/p&gt;

&lt;p&gt;Same in distributed systems. The database slowdown isn't what takes you down. It's the retry storm from 400 service instances hammering the recovering database that takes you down.&lt;/p&gt;

&lt;p&gt;Good divers train for &lt;em&gt;compound&lt;/em&gt; failures: light out &lt;em&gt;and&lt;/em&gt; low on gas, lost line &lt;em&gt;and&lt;/em&gt; silted visibility. Good systems are designed for compound failures too: circuit breakers, exponential backoff with jitter, bulkheads, and graceful degradation. Not because the first failure is rare, but because the second one, triggered by your response to the first, is where the real damage happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn pressure is a circuit breaker
&lt;/h2&gt;

&lt;p&gt;Before a cave dive, you calculate your "turn pressure" — the tank pressure at which you stop going in and start coming out, regardless of how close you are to the thing you wanted to see. It's non-negotiable. You don't get to feel your way through it.&lt;/p&gt;

&lt;p&gt;Circuit breakers work the same way. You pick a threshold in advance, when you're calm and have a clear head. And when the threshold trips, the system doesn't get to argue with it. It just turns around.&lt;/p&gt;

&lt;p&gt;The hardest part of both is the same: &lt;em&gt;accepting the limit you set for yourself when you were thinking clearly, even when the situation makes you want to push past it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklists feel stupid until they save you
&lt;/h2&gt;

&lt;p&gt;Every cave diver I respect uses a pre-dive checklist. Not because they forget things — but because under stress, everyone forgets things. The checklist is what your past, calm self leaves behind to protect your future, stressed self.&lt;/p&gt;

&lt;p&gt;Runbooks are the same. The incident is not the time to remember the command. The deployment at 2 am is not the time to improvise the rollback procedure. Write it down when it's quiet. Read it when it's loud.&lt;/p&gt;

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

&lt;p&gt;Both disciplines teach you the same uncomfortable thing: &lt;strong&gt;most disasters are built in advance, by people who assumed the happy path was the only path.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The habits that keep you alive in a cave are the same ones that keep systems running at 3 am on a Saturday. Redundancy, calm limits, planning for the compound failure, trusting your past self's checklist over your present self's instincts.&lt;/p&gt;

&lt;p&gt;The costume is different. The physics of failure is the same.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you're into either distributed systems or cave diving, I'd love to hear what overlaps you've noticed. Always surprising how many fields converge on the same answers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>softwareengineering</category>
      <category>backend</category>
      <category>career</category>
    </item>
    <item>
      <title>One TUI for RabbitMQ, Kafka, and MQTT: why I built queuepeek</title>
      <dc:creator>Matías Denda</dc:creator>
      <pubDate>Wed, 22 Apr 2026 13:00:00 +0000</pubDate>
      <link>https://forem.com/mdenda/one-tui-for-rabbitmq-kafka-and-mqtt-why-i-built-queuepeek-1ldn</link>
      <guid>https://forem.com/mdenda/one-tui-for-rabbitmq-kafka-and-mqtt-why-i-built-queuepeek-1ldn</guid>
      <description>&lt;h2&gt;
  
  
  The problem I was trying to solve
&lt;/h2&gt;

&lt;p&gt;I work across a few projects that all talk to message brokers, but never the same one. Some services are on RabbitMQ. The data pipeline runs on Kafka. A handful of IoT integrations use MQTT. Normal stuff.&lt;/p&gt;

&lt;p&gt;What wasn't normal was the amount of context-switching every time something went wrong in production. Three different web UIs, three different mental models, three different ways to peek at a message without accidentally consuming it.&lt;/p&gt;

&lt;p&gt;The RabbitMQ Management UI is fine, but it's a web app — and half the time I'm already in a terminal next to the logs. Kafka UIs are a whole can of worms (every company seems to use a different one). MQTT doesn't really have a good "just let me see what's retained on this topic" tool for free.&lt;/p&gt;

&lt;p&gt;So after one too many incidents where I wanted to diff two DLQ messages and ended up pasting JSON into an online diff tool, I started building what became &lt;strong&gt;queuepeek&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;queuepeek is a terminal UI written in Rust (on top of &lt;a href="https://github.com/ratatui-org/ratatui" rel="noopener noreferrer"&gt;ratatui&lt;/a&gt;) that speaks RabbitMQ, Kafka, and MQTT from the same interface. You launch it, pick a profile, drill down through queues/topics, and land on individual messages.&lt;/p&gt;

&lt;p&gt;The entire thing is keyboard-driven and follows the same wizard flow regardless of the broker:&lt;/p&gt;

&lt;p&gt;Profiles -&amp;gt; Queues/Topics -&amp;gt; Messages -&amp;gt; Message Detail&lt;/p&gt;

&lt;p&gt;Esc always pops one level up. &lt;code&gt;/&lt;/code&gt; always filters. &lt;code&gt;?&lt;/code&gt; always shows help contextual to where you are and which broker you're on.&lt;/p&gt;

&lt;p&gt;No mouse. No tabs. No switching apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design choices that paid off
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Non-destructive peek
&lt;/h3&gt;

&lt;p&gt;This was the whole point. A "queue inspector" that consumes messages while you're reading them is not an inspector — it's a silent bug waiting to happen.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For &lt;strong&gt;RabbitMQ&lt;/strong&gt;, queuepeek uses the Management HTTP API's &lt;code&gt;get&lt;/code&gt; endpoint with &lt;code&gt;ack_requeue_true&lt;/code&gt;. You read, the message stays.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;Kafka&lt;/strong&gt;, every read session spins up an ephemeral consumer with a unique group ID. You're not stealing offsets from anyone.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;MQTT&lt;/strong&gt;, you're subscribing to a topic so it's inherently non-mutating, but retained message management has its own explicit screen with a clear "this will clear the retained payload" confirmation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sounds basic. A surprising number of tools don't do this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Same keybindings, different brokers
&lt;/h3&gt;

&lt;p&gt;Kafka doesn't have queues, it has topics. RabbitMQ has exchanges and bindings. MQTT has topic hierarchies. Under the hood these are very different beasts, but from the user's point of view, "show me what's in here" should feel the same.&lt;/p&gt;

&lt;p&gt;The footer at the bottom of every screen dynamically filters shortcuts to the backend you're connected to — &lt;code&gt;G:groups&lt;/code&gt; only shows on Kafka, &lt;code&gt;X:topology&lt;/code&gt; only on RabbitMQ, &lt;code&gt;H:retained&lt;/code&gt; only on MQTT. No dead keys, no "this doesn't work on your broker" errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-select bulk ops
&lt;/h3&gt;

&lt;p&gt;Checkboxes in a TUI sound fiddly, but they turned out to be one of the most useful features. &lt;code&gt;Space&lt;/code&gt; toggles selection on the current message, then any operation you trigger (delete, copy, move, export) applies to all selected messages — streamed, not loaded into memory.&lt;/p&gt;

&lt;p&gt;Want to delete 10,000 messages from a DLQ after confirming none of them match a pattern you care about? Filter, select all, delete. Done from the keyboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few features I'm particularly happy with
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Side-by-side message diff.&lt;/strong&gt; Select two messages, press &lt;code&gt;d&lt;/code&gt;, get a colored diff. Uses the &lt;a href="https://github.com/mitsuhiko/similar" rel="noopener noreferrer"&gt;&lt;code&gt;similar&lt;/code&gt;&lt;/a&gt; crate. Embarrassingly useful for "why does this one message fail and the other succeed?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema Registry integration.&lt;/strong&gt; If you configure a Confluent-compatible Schema Registry URL, queuepeek auto-decodes Avro and raw Protobuf payloads using the Confluent wire format (magic byte &lt;code&gt;0x00&lt;/code&gt; + 4-byte schema ID + body). Toggle raw/decoded with&lt;br&gt;
  &lt;code&gt;s&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real concurrent benchmarking.&lt;/strong&gt; Press &lt;code&gt;F5&lt;/code&gt; on a queue to run a flood-publish benchmark with N worker threads (via &lt;code&gt;std::thread::scope&lt;/code&gt;), rendering a live gauge and p50/p95/p99 latency percentiles at the end. Useful for capacity-planning conversations&lt;br&gt;
  that usually start with "how fast can this queue take?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Webhook alerts.&lt;/strong&gt; Configure a regex pattern in &lt;code&gt;config.toml&lt;/code&gt;, point it at a webhook URL, and queuepeek polls every 30 seconds and POSTs on match (deduplicated by message hash so you don't spam yourself). Good for catching a specific error pattern appearing in a queue before it becomes an incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payload templates with interpolation.&lt;/strong&gt; &lt;code&gt;Ctrl+T&lt;/code&gt; to save the current message, &lt;code&gt;Ctrl+W&lt;/code&gt; to insert it. Supports variables like &lt;code&gt;{{timestamp}}&lt;/code&gt;, &lt;code&gt;{{uuid}}&lt;/code&gt;, &lt;code&gt;{{random_int}}&lt;/code&gt;, &lt;code&gt;{{counter}}&lt;/code&gt;, and &lt;code&gt;{{env.VAR}}&lt;/code&gt; for anything you export in your shell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DLQ reroute.&lt;/strong&gt; RabbitMQ x-death headers get parsed and displayed, and &lt;code&gt;L&lt;/code&gt; re-routes a message back to its original exchange. Mostly because I got tired of manually copy-pasting routing keys out of &lt;code&gt;x-death&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Interesting implementation bits
&lt;/h2&gt;

&lt;p&gt;A few things that weren't obvious going in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ratatui + crossterm + mpsc channels&lt;/strong&gt; is all you need for a responsive TUI. Background I/O (broker calls, file operations, webhook polling) runs in &lt;code&gt;std::thread&lt;/code&gt; workers and posts results back through a channel that the event loop drains each tick.&lt;br&gt;
No async runtime, no Tokio. The whole app feels instant.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Auto-refresh is dumb and works.&lt;/strong&gt; Queue list refreshes every 5 seconds, message list every time tail mode is on. No WebSockets, no push. The Management API is cheap enough that polling is fine.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scheduled messages persist to disk.&lt;/strong&gt; &lt;code&gt;~/.config/queuepeek/scheduled.json&lt;/code&gt; with epoch seconds. If you schedule a publish and the app crashes or you close it, the schedule survives.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;79 unit tests&lt;/strong&gt; across filters, comparison, operations, schema decoding, and config. TUI logic is hard to test end-to-end, but the pure functions underneath are easy.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# From crates.io (needs cmake for librdkafka)&lt;/span&gt;
cargo &lt;span class="nb"&gt;install &lt;/span&gt;queuepeek
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or grab a prebuilt binary: &lt;a href="https://github.com/matutetandil/queuepeek/releases" rel="noopener noreferrer"&gt;https://github.com/matutetandil/queuepeek/releases&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Supported platforms: macOS (ARM/Intel), Linux (x86), Windows (x86/ARM). Linux ARM builds need cargo install because cross-compiling librdkafka is its own adventure.&lt;/p&gt;

&lt;p&gt;Minimal ~/.config/queuepeek/config.toml:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[profiles.local]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"rabbitmq"&lt;/span&gt;
&lt;span class="py"&gt;host&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"localhost"&lt;/span&gt;
&lt;span class="py"&gt;port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15672&lt;/span&gt;
&lt;span class="py"&gt;username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"guest"&lt;/span&gt;
&lt;span class="py"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"guest"&lt;/span&gt;
&lt;span class="py"&gt;vhost&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/matutetandil/queuepeek" rel="noopener noreferrer"&gt;https://github.com/matutetandil/queuepeek&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docs: /docs folder covers configuration, keyboard shortcuts, backends, and architecture.&lt;/p&gt;

&lt;p&gt;What I'd love feedback on&lt;/p&gt;

&lt;p&gt;It's a solo side project and I'm the main user. That means the keybindings reflect my own muscle memory, the defaults reflect my own workflows, and the rough edges are the ones I don't personally bump into.&lt;/p&gt;

&lt;p&gt;If you spend time in any of these brokers and something feels off — a missing shortcut, a feature you'd expect, a decoding format I'm not handling — please open an issue. Especially on MQTT, which I use least.&lt;/p&gt;

&lt;p&gt;MIT licensed. No telemetry. No signup. Just a binary that reads queues.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>devops</category>
      <category>showdev</category>
      <category>terminal</category>
    </item>
    <item>
      <title>What actually happens when you `git merge --no-ff`</title>
      <dc:creator>Matías Denda</dc:creator>
      <pubDate>Tue, 21 Apr 2026 14:48:28 +0000</pubDate>
      <link>https://forem.com/mdenda/what-actually-happens-when-you-git-merge-no-ff-4il1</link>
      <guid>https://forem.com/mdenda/what-actually-happens-when-you-git-merge-no-ff-4il1</guid>
      <description>&lt;p&gt;Most developers use &lt;code&gt;git merge&lt;/code&gt; without ever thinking about what's happening internally. Then one day they see &lt;code&gt;--no-ff&lt;/code&gt; in a team's workflow documentation, Google it, read three Stack Overflow answers, and walk away with a vague sense that "it creates a merge commit or something."&lt;/p&gt;

&lt;p&gt;This post is the version I wish I'd read earlier. Two diagrams, one clear distinction, and why it actually matters for your team.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;You're on &lt;code&gt;main&lt;/code&gt;. Your coworker merged their feature. You branched off, added two commits, and now it's time to merge your branch back. What happens next depends on one flag.&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;# You're here&lt;/span&gt;
git checkout main
git merge feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git has two ways to integrate your feature branch into &lt;code&gt;main&lt;/code&gt;. The one it picks by default depends on whether the branches have &lt;em&gt;diverged&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 1: Fast-forward (the default, when possible)
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;main&lt;/code&gt; hasn't moved since you branched off, Git doesn't create a new commit. It just moves the &lt;code&gt;main&lt;/code&gt; pointer forward to the tip of your feature branch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2bentvl8nu0tmo6qx8gp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2bentvl8nu0tmo6qx8gp.png" alt="Fast-forward merge: main pointer simply moves forward, no new commit" width="800" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's it. No merge commit. The feature branch and &lt;code&gt;main&lt;/code&gt; now point to the same commit. If you look at &lt;code&gt;git log&lt;/code&gt; on &lt;code&gt;main&lt;/code&gt;, it reads like D and E were always there. The branch effectively disappears from history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 2: &lt;code&gt;--no-ff&lt;/code&gt; (always create a merge commit)
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;--no-ff&lt;/code&gt;, Git creates an explicit merge commit even when a fast-forward was possible:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnxei2tu2p244tldnal7j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnxei2tu2p244tldnal7j.png" alt="no-ff merge: a new merge commit M is created with two parents" width="800" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;M&lt;/code&gt; is a new commit whose parents are &lt;code&gt;C&lt;/code&gt; (the previous tip of main) and &lt;code&gt;E&lt;/code&gt; (the tip of feature). It has no code changes of its own — its diff is empty — but it records that &lt;em&gt;these commits were integrated together at this point&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this distinction matters
&lt;/h2&gt;

&lt;p&gt;The two histories above contain the same code. So does it matter? Yes, and here's where it bites real teams.&lt;/p&gt;

&lt;h3&gt;
  
  
  It matters for &lt;code&gt;git bisect&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;git bisect&lt;/code&gt; helps you find which commit introduced a bug by doing a binary search through history. With fast-forward merges, the search descends into individual feature commits — you might land on a half-finished refactor where the bug is genuinely present but so is a broken test, making the bisect useless.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;--no-ff&lt;/code&gt;, you can run &lt;code&gt;git bisect --first-parent&lt;/code&gt; and bisect &lt;em&gt;merge commits only&lt;/em&gt;, treating each feature as an atomic unit. Found the regression? You know which feature to revert, not which arbitrary mid-feature commit to blame.&lt;/p&gt;

&lt;h3&gt;
  
  
  It matters for &lt;code&gt;git revert&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;If you merged with &lt;code&gt;--no-ff&lt;/code&gt; and need to roll back the feature, you revert the single merge commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git revert &lt;span class="nt"&gt;-m&lt;/span&gt; 1 &amp;lt;merge-commit-hash&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That undoes all of D and E in one go. With fast-forward, you'd need to revert each commit individually — or figure out which commits belonged to the feature in the first place.&lt;/p&gt;

&lt;h3&gt;
  
  
  It matters for reading history
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;git log --graph --first-parent main&lt;/code&gt; with &lt;code&gt;--no-ff&lt;/code&gt; merges shows you a clean list of features integrated into main, one per line. Without merge commits, the log is a flat stream of every individual commit ever made. For a large team, the difference is between "I can see what shipped last week" and "good luck."&lt;/p&gt;

&lt;h2&gt;
  
  
  What GitHub and GitLab do
&lt;/h2&gt;

&lt;p&gt;When you click "Merge pull request" on GitHub or GitLab, they default to creating a merge commit (&lt;code&gt;--no-ff&lt;/code&gt; behavior). The "Rebase and merge" and "Squash and merge" options exist too, but the default merge commit exists precisely because of the benefits above.&lt;/p&gt;

&lt;p&gt;This is why teams that use the GitHub/GitLab UI religiously often have cleaner history than teams that merge locally on the command line — the UI forces a pattern that the command line leaves optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  When fast-forward is fine
&lt;/h2&gt;

&lt;p&gt;For throwaway branches, personal experiments, or single-commit fixes where the commit already tells the whole story, fast-forward is perfectly appropriate. The rule of thumb I use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single commit fix&lt;/strong&gt; → fast-forward is fine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature branch with 2+ commits&lt;/strong&gt; → &lt;code&gt;--no-ff&lt;/code&gt; preserves the grouping&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release branch merge&lt;/strong&gt; → always &lt;code&gt;--no-ff&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hotfix branch merge&lt;/strong&gt; → always &lt;code&gt;--no-ff&lt;/code&gt; (you want revertability)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Making it a team default
&lt;/h2&gt;

&lt;p&gt;If you want the team to use &lt;code&gt;--no-ff&lt;/code&gt; consistently, either set it at the repo level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; merge.ff &lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or — better — require it via branch protection rules on your hosting platform. That way nobody's local config can bypass it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is adapted from a chapter of &lt;em&gt;&lt;a href="https://mdenda.gumroad.com/l/git-in-depth" rel="noopener noreferrer"&gt;Git in Depth: From Solo Developer to Engineering Teams&lt;/a&gt;&lt;/em&gt;, a 658-page book I just released on Git for working developers — from day-to-day tools to CI/CD, branching strategies, and Git at organizational scale. Launch price $29 with code &lt;code&gt;EARLYBIRD&lt;/code&gt; (first 100 copies).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next week: &lt;em&gt;git worktree — the stash replacement nobody teaches you&lt;/em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>git</category>
      <category>devops</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Automatic Node.js Version Switching Across Projects</title>
      <dc:creator>Matías Denda</dc:creator>
      <pubDate>Fri, 07 Nov 2025 21:21:15 +0000</pubDate>
      <link>https://forem.com/mdenda/automatic-nodejs-version-switching-across-projects-2dae</link>
      <guid>https://forem.com/mdenda/automatic-nodejs-version-switching-across-projects-2dae</guid>
      <description>&lt;h1&gt;
  
  
  The Challenge
&lt;/h1&gt;

&lt;p&gt;Modern JavaScript development often involves working on multiple projects simultaneously. Each project may require a different Node.js version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Legacy project: Node 14&lt;/li&gt;
&lt;li&gt;Production app: Node 18 LTS&lt;/li&gt;
&lt;li&gt;New experimental project: Node 20&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Manually switching versions between projects adds friction to your workflow. You need to remember which version each project uses and run the appropriate commands when switching contexts.&lt;/p&gt;

&lt;h1&gt;
  
  
  What is AutoNode?
&lt;/h1&gt;

&lt;p&gt;AutoNode is a CLI tool that automatically detects and switches to the correct Node.js version when you navigate between projects. It works seamlessly with your existing version manager (nvm, nvs, or Volta).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔍 Automatic version detection from multiple sources&lt;/li&gt;
&lt;li&gt;⚡ Zero runtime dependencies (single native binary)&lt;/li&gt;
&lt;li&gt;🔄 Works with your existing version manager&lt;/li&gt;
&lt;li&gt;🌍 Cross-platform (macOS, Linux, Windows)&lt;/li&gt;
&lt;li&gt;📦 Under 6MB with everything included&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  Version Detection
&lt;/h3&gt;

&lt;p&gt;AutoNode reads version requirements from multiple sources (in priority order):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.nvmrc&lt;/code&gt;&lt;/strong&gt; - Standard nvm configuration file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.node-version&lt;/code&gt;&lt;/strong&gt; - Alternative version file format&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;package.json&lt;/code&gt;&lt;/strong&gt; - Reads &lt;code&gt;engines.node&lt;/code&gt; field&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Dockerfile&lt;/code&gt;&lt;/strong&gt; - Parses &lt;code&gt;FROM node:X&lt;/code&gt; declarations&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This multi-source approach ensures compatibility with different project conventions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shell Integration
&lt;/h3&gt;

&lt;p&gt;When you &lt;code&gt;cd&lt;/code&gt; into a project directory, AutoNode:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detects the required Node.js version&lt;/li&gt;
&lt;li&gt;Checks which version manager you have installed&lt;/li&gt;
&lt;li&gt;Switches to the appropriate version automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The integration works through a shell hook similar to other developer tools like direnv or starship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One-line install:&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;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/matutetandil/autonode/main/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detect your platform and architecture&lt;/li&gt;
&lt;li&gt;Download the appropriate binary&lt;/li&gt;
&lt;li&gt;Install to a standard location&lt;/li&gt;
&lt;li&gt;Configure shell integration for bash/zsh/fish&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Manual Commands
&lt;/h2&gt;

&lt;p&gt;You can also use AutoNode manually:&lt;/p&gt;

&lt;h3&gt;
  
  
  Check detected version without switching
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;autonode &lt;span class="nt"&gt;--check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Force reinstall detected version
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;autonode &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Update AutoNode itself
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  autonode update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Real-World Use Cases
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Development Teams
&lt;/h2&gt;

&lt;p&gt;Ensure all team members use consistent Node.js versions across projects without manual intervention.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD Pipelines
&lt;/h2&gt;

&lt;p&gt;Automatically use the correct Node version in build scripts without hardcoding version numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monorepo Management
&lt;/h2&gt;

&lt;p&gt;Different packages in a monorepo can specify different Node requirements, and AutoNode handles transitions seamlessly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker Development
&lt;/h2&gt;

&lt;p&gt;Parse Node versions directly from Dockerfiles to match your local environment with production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compatibility
&lt;/h2&gt;

&lt;p&gt;Operating Systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;macOS (Intel &amp;amp; Apple Silicon)&lt;/li&gt;
&lt;li&gt;Linux (AMD64 &amp;amp; ARM64)&lt;/li&gt;
&lt;li&gt;Windows (AMD64)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Version Managers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nvm (Node Version Manager)&lt;/li&gt;
&lt;li&gt;nvs (Node Version Switcher)&lt;/li&gt;
&lt;li&gt;Volta&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shells:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bash&lt;/li&gt;
&lt;li&gt;zsh&lt;/li&gt;
&lt;li&gt;fish&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Roadmap
&lt;/h1&gt;

&lt;p&gt;Planned improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Additional version manager support (fnm, asdf)&lt;/li&gt;
&lt;li&gt;Configuration file for custom behavior&lt;/li&gt;
&lt;li&gt;Shell completion scripts&lt;/li&gt;
&lt;li&gt;Package manager distribution (Homebrew, apt)&lt;/li&gt;
&lt;li&gt;Pre-commit hooks integration&lt;/li&gt;
&lt;li&gt;CI/CD action/workflow templates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Contributing&lt;/p&gt;

&lt;p&gt;AutoNode is open source and welcomes contributions. Whether it's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bug reports and fixes&lt;/li&gt;
&lt;li&gt;New version detector implementations&lt;/li&gt;
&lt;li&gt;Version manager integrations&lt;/li&gt;
&lt;li&gt;Documentation improvements&lt;/li&gt;
&lt;li&gt;Feature requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check out the &lt;a href="https://github.com/matutetandil/autonode" rel="noopener noreferrer"&gt;https://github.com/matutetandil/autonode&lt;/a&gt; to get involved.&lt;/p&gt;

&lt;h1&gt;
  
  
  Try It Out
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Installation:
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/matutetandil/autonode/main/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quick test:
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nb"&gt;mkdir &lt;/span&gt;test-project &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;test-project
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"18.17.0"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .nvmrc
  &lt;span class="c"&gt;# AutoNode switches automatically&lt;/span&gt;
  node &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Feedback Wanted:
&lt;/h1&gt;

&lt;p&gt;I'm actively looking for feedback on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does it work reliably with your setup?&lt;/li&gt;
&lt;li&gt;Any edge cases in version detection?&lt;/li&gt;
&lt;li&gt;Performance in large monorepos?&lt;/li&gt;
&lt;li&gt;Installation experience on different platforms?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feel free to open an issue or start a discussion on GitHub!&lt;/p&gt;

&lt;p&gt;Resources&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Repository: &lt;a href="https://github.com/matutetandil/autonode" rel="noopener noreferrer"&gt;https://github.com/matutetandil/autonode&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Documentation: Full setup and usage guide in the README&lt;/li&gt;
&lt;li&gt;Installation Script: Direct download and shell integration&lt;/li&gt;
&lt;li&gt;Releases: Pre-built binaries for all platforms&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;AutoNode aims to simplify Node.js version management across projects. If you find it useful or have suggestions, I'd love to hear from you!&lt;/p&gt;




</description>
      <category>go</category>
      <category>node</category>
      <category>cli</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
