<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[The Polymathic Engineer]]></title><description><![CDATA[A newsletter about Algorithms, Computer Vision, and Distributed Systems]]></description><link>https://newsletter.francofernando.com</link><image><url>https://substackcdn.com/image/fetch/$s_!WkEZ!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F157b59b0-a7e4-4f31-8d83-9a2034b2ff4e_354x354.png</url><title>The Polymathic Engineer</title><link>https://newsletter.francofernando.com</link></image><generator>Substack</generator><lastBuildDate>Fri, 24 Jul 2026 14:34:50 GMT</lastBuildDate><atom:link href="https://newsletter.francofernando.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Franco Fernando]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[francofernando@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[francofernando@substack.com]]></itunes:email><itunes:name><![CDATA[Franco Fernando]]></itunes:name></itunes:owner><itunes:author><![CDATA[Franco Fernando]]></itunes:author><googleplay:owner><![CDATA[francofernando@substack.com]]></googleplay:owner><googleplay:email><![CDATA[francofernando@substack.com]]></googleplay:email><googleplay:author><![CDATA[Franco Fernando]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[The Perceptron]]></title><description><![CDATA[How the simplest classification model works, and why it is the building block of every neural network.]]></description><link>https://newsletter.francofernando.com/p/the-perceptron</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/the-perceptron</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 18 Jul 2026 05:44:10 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!wgoL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41c5bf0b-8d55-4756-a58a-0cff84e5863f_950x940.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Welcome to the 182nd issue of the Polymathic Engineer newsletter.</p><p><a href="https://newsletter.francofernando.com/p/linear-regression">In a recent article</a>, we covered linear regression: how a machine learns to draw a line through your data and guess a number, like the salary of an engineer. But many of the questions we worry about in practice don&#8217;t have a number as an answer. Is this email spam or not? Is this patient sick or healthy? Will this user click or not?</p><p>Guessing a category rather than a number is known as classification, and it is one of the most important areas of machine learning. In this issue, we look at the oldest and simplest classification model: the perceptron. Invented in 1957, the perceptron is the building block of every neural network in use today. Once you grasp how a perceptron works, you have done a good part of the work needed to understand deep learning.</p><p>The outline is as follows:</p><ul><li><p>Building an Intuition: Opinion Mining</p></li><li><p>Why We Need a Bias</p></li><li><p>Weights, the Score, and the Step Function</p></li><li><p>More Than Two Words: Hyperplanes</p></li><li><p>How Good Is a Classifier? The Perceptron Error</p></li><li><p>The Perceptron Trick and the Perceptron Algorithm</p></li><li><p>A Bit of History: Where Perceptrons Fall Short</p></li><li><p>Putting Everything Into Practice</p></li></ul><div><hr></div><h4><strong><a href="https://www.eventbrite.co.uk/e/arc-2026-software-architecture-in-the-age-of-ai-tickets-1991591030384?aff=polymathicnl"><span>How Is AI Changing Software Architecture?</span></a></strong></h4><p>AI is introducing new architectural questions. Other than models and frameworks, architects are now considering governance, reliability, agentic systems, inference at scale, and how AI fits within existing software ecosystems.</p><p><strong><a href="https://www.eventbrite.co.uk/e/arc-2026-software-architecture-in-the-age-of-ai-tickets-1991591030384?aff=polymathicnl">ARC 2026: Software Architecture in the Age of AI</a></strong> is an event that brings together software architects and engineering leaders from <strong>Google, AWS, Salesforce, Netflix</strong>, and other leading technology organizations to share how they&#8217;re tackling these challenges.</p><p>If these are conversations you are having within your team, the agenda is worth exploring.</p><p><strong><span>25-26 July (Virtual)</span></strong></p><p>Register here: <a href="https://www.eventbrite.co.uk/e/arc-2026-software-architecture-in-the-age-of-ai-tickets-1991591030384?aff=polymathicnl">ARC 2026</a></p><p><span>Use the code</span><strong><span> POLYMATHIC50 to get 50% off </span></strong><span>(Valid for the first 10 users)</span></p><div><hr></div><h2>Building an Intuition: Opinion Mining</h2><p>Let&#8217;s suppose that your company launches an app in a foreign market, and the reviews start coming in. Each review has a star rating, so you know which ones are happy and which ones are angry. However, the reviews are written in a language you don&#8217;t speak, and you would love to know what makes a review positive or negative.</p><p>You notice that two words keep showing up, taka and bruk, so you pick 4 reviews and count how many times each word appears:</p><ul><li><p>&#8220;Taka taka taka!&#8221; (5 stars): taka 3 times, bruk 0 times</p></li><li><p>&#8220;Bruk bruk bruk!&#8221; (1 star): taka 0 times, bruk 3 times</p></li><li><p>&#8220;Taka taka bruk!&#8221; (5 stars): taka 2 times, bruk 1 time</p></li><li><p>&#8220;Bruk taka bruk!&#8221; (1 star): taka 1 time, bruk 2 times</p></li></ul><p>Then a new review arrives: &#8220;Taka bruk taka taka!&#8221;. There is no star rating yet, so what do you think it says? Likely, something good. Although you don&#8217;t speak the language, you can observe that taka appears more in the positive reviews and bruk more in the angry reviews.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!wgoL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41c5bf0b-8d55-4756-a58a-0cff84e5863f_950x940.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wgoL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41c5bf0b-8d55-4756-a58a-0cff84e5863f_950x940.png 424w, https://substackcdn.com/image/fetch/$s_!wgoL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41c5bf0b-8d55-4756-a58a-0cff84e5863f_950x940.png 848w, https://substackcdn.com/image/fetch/$s_!wgoL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41c5bf0b-8d55-4756-a58a-0cff84e5863f_950x940.png 1272w, https://substackcdn.com/image/fetch/$s_!wgoL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41c5bf0b-8d55-4756-a58a-0cff84e5863f_950x940.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wgoL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41c5bf0b-8d55-4756-a58a-0cff84e5863f_950x940.png" width="356" height="352.2526315789474" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/41c5bf0b-8d55-4756-a58a-0cff84e5863f_950x940.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:940,&quot;width&quot;:950,&quot;resizeWidth&quot;:356,&quot;bytes&quot;:73904,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/207001344?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41c5bf0b-8d55-4756-a58a-0cff84e5863f_950x940.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!wgoL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41c5bf0b-8d55-4756-a58a-0cff84e5863f_950x940.png 424w, https://substackcdn.com/image/fetch/$s_!wgoL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41c5bf0b-8d55-4756-a58a-0cff84e5863f_950x940.png 848w, https://substackcdn.com/image/fetch/$s_!wgoL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41c5bf0b-8d55-4756-a58a-0cff84e5863f_950x940.png 1272w, https://substackcdn.com/image/fetch/$s_!wgoL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41c5bf0b-8d55-4756-a58a-0cff84e5863f_950x940.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>We can turn this intuition into a rule. Give each word a score: taka gets +1 point, and bruk gets -1 point. To classify a review, add up the scores of all its words. If the total is zero or more, predict the review is happy. If it is negative, predict it is angry. The new review scores 3 - 1 = 2, so we predict it is happy.</p><p>The rule we just made is a perceptron. There is also a nice way to see it geometrically. If we plot each review on a 2D graph, with the number of times taka appears on the horizontal axis and bruk on the vertical axis, the happy reviews end up on the bottom right and the angry ones on the top left. The two groups are divided by the line where both words appear the same number of times. Everything below the line means happy, and everything above means angry.</p><p>That is what a perceptron does: it draws a line that separates the two classes of points.</p><h2>Why We Need a Bias</h2><p>A few months later, your app takes off in another country. Again, the reviews arrive in a language you don&#8217;t speak, and two words keep popping up: miko and zumi. You pick 6 reviews and count how many times each word appears:</p><ul><li><p>&#8220;Miko!&#8221; (1 star): miko 1 time, zumi 0 times</p></li><li><p>&#8220;Zumi zumi!&#8221; (1 star): miko 0 times, zumi 2 times</p></li><li><p>&#8220;Miko zumi miko!&#8221; (1 star): miko 2 times, zumi 1 time</p></li><li><p>&#8220;Zumi miko zumi zumi!&#8221; (5 stars): miko 1 time, zumi 3 times</p></li><li><p>&#8220;Miko zumi miko zumi!&#8221; (5 stars): miko 2 times, zumi 2 times</p></li><li><p>&#8220;Miko miko zumi zumi miko!&#8221; (5 stars): miko 3 times, zumi 2 times</p></li></ul><p>Let&#8217;s try the same approach and give each word a score. However, this time something doesn&#8217;t add up. Both words appear in happy and angry reviews, so neither one seems positive or negative on its own. But if you look closer, you can see a different pattern: all the angry reviews are short, while all the happy reviews are long. When people are sad in this country, they drop a few words and move on. When people are happy, they write more. What decides the mood is not which words appear, but how many.</p><p>We can turn this into a rule, too. Give both words a score of +1, then add them up and guess &#8220;happy&#8221; if the sum is 4 or more. To make the math easier later, we use a cutoff of 3.5 instead: happy if the score is above 3.5, angry if it is below.</p><p>However, most of the time, perceptrons are not written by comparing the score to a cutoff. The standard way is to move the cutoff into the score itself. Instead of asking &#8220;is the score at least 3.5?&#8221;, we add -3.5 to the score and ask &#8220;is the result at least 0?&#8221;. The first question is the same as the second, but the second always compares to zero, regardless of the dataset. This extra number is known as the bias, and it&#8217;s the negative of the cutoff.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_GYl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e1f8ebd-44f4-42d0-94f4-192eb35ab40b_1052x1106.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_GYl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e1f8ebd-44f4-42d0-94f4-192eb35ab40b_1052x1106.png 424w, https://substackcdn.com/image/fetch/$s_!_GYl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e1f8ebd-44f4-42d0-94f4-192eb35ab40b_1052x1106.png 848w, https://substackcdn.com/image/fetch/$s_!_GYl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e1f8ebd-44f4-42d0-94f4-192eb35ab40b_1052x1106.png 1272w, https://substackcdn.com/image/fetch/$s_!_GYl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e1f8ebd-44f4-42d0-94f4-192eb35ab40b_1052x1106.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_GYl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e1f8ebd-44f4-42d0-94f4-192eb35ab40b_1052x1106.png" width="338" height="355.34980988593156" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8e1f8ebd-44f4-42d0-94f4-192eb35ab40b_1052x1106.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1106,&quot;width&quot;:1052,&quot;resizeWidth&quot;:338,&quot;bytes&quot;:82686,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/207001344?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e1f8ebd-44f4-42d0-94f4-192eb35ab40b_1052x1106.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_GYl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e1f8ebd-44f4-42d0-94f4-192eb35ab40b_1052x1106.png 424w, https://substackcdn.com/image/fetch/$s_!_GYl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e1f8ebd-44f4-42d0-94f4-192eb35ab40b_1052x1106.png 848w, https://substackcdn.com/image/fetch/$s_!_GYl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e1f8ebd-44f4-42d0-94f4-192eb35ab40b_1052x1106.png 1272w, https://substackcdn.com/image/fetch/$s_!_GYl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e1f8ebd-44f4-42d0-94f4-192eb35ab40b_1052x1106.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The bias has a nice interpretation: it is the score of an empty review. A review with no words is marked as &#8220;happy&#8221; if the bias is positive and &#8220;angry&#8221; if it is negative. In our case, the bias is -3.5, and that matches the data: users in this country who say nothing extra are likely upset. You can find both situations in the real world. In app stores, an empty review usually comes with 5 stars, since happy users just tap a rating and leave (positive bias). On the other hand, no one opens a support ticket to say that everything works fine (negative bias).</p><p>In terms of geometry, the bias moves the line away from the origin. The first dataset&#8217;s line went through the point (0, 0); this one crosses the axes at 3.5. The weights decide the direction of the line, and the bias decides where it sits.</p><h2>Weights, the Score, and the Step Function</h2><p>Now that we have built two classifiers by hand, it is a good moment to give the pieces their official names. The counts of each word are the features, which are what we use to guess. The scores we assigned to the words are known as the weights, and are what the model learns from the data together with the bias. If this sounds like something you have seen before, it&#8217;s because you have seen it in the<a href="https://newsletter.francofernando.com/p/linear-regression"> linear regression article</a>. The score of a review is calculated with the same formula we used to guess a salary:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;73e19615-4e98-4c1f-9e7a-7f628b3205c0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">score = w1 &#215; x1 + w2 &#215; x2 + bias</code></pre></div><p>where x1 and x2 are the number of times each word appears, and w1 and w2 are their weights.</p><p>One more thing is needed: turning the labels into numbers. Machine learning models work with numbers, not with moods, so from now on &#8220;happy&#8221; becomes 1 and &#8220;angry&#8221; becomes 0. To make the prediction match, we use a function called the step function. The step function returns 1 if its input is 0 or more, and 0 if its input is negative:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;41e2eb1d-3b4f-4feb-913d-12fa5015af06&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">prediction = step(w1 &#215; x1 + w2 &#215; x2 + bias)</code></pre></div><p>That is the whole perceptron: weights, a bias, and a step function. The only difference from linear regression is in that last step. Linear regression returns the score directly since that is what we want, a number. The perceptron squashes the score into a 1 or a 0, since we want a category.</p><p>The step function is also what is called an activation function, a concept that plays a huge role in neural networks. For now, you can think of it as the function that turns a score into a &#8220;yes&#8221; or a &#8220;no&#8221;.</p><h2>More Than Two Words: Hyperplanes</h2><p>Our two examples used languages with only two words each, which is helpful because we can draw every review as a point on a 2D graph. However, real languages have a lot of words. What happens to the perceptron then?</p><p>In terms of the formula, there are no changes. Each word has a value assigned, and the score stays the same even if it has more terms:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;e256edf8-a778-4774-9f49-825d60195fc4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">score = w1 &#215; x1 + w2 &#215; x2 + ... + wn &#215; xn + bias</code></pre></div><p>In terms of geometry, the picture grows by one dimension for every word. With three words, the reviews turn into points in 3D space, and the perceptron splits them with a plane instead of a line. With n words, the points live in n-dimensional space, and the line between them is called a hyperplane. We can&#8217;t picture it anymore, but the idea is the same: there is a flat surface dividing the space into a happy and an angry side.</p><p>If the idea of hyperplanes seems too abstract, here is a mental picture that helps. Let&#8217;s imagine building the classifier for English. You have to go over the whole dictionary and assign a score to every single word: &#8220;amazing&#8221; gets +9, &#8220;refund&#8221; gets -6, &#8220;the&#8221; gets 0.1, and so on. When you want to classify a review, you add up the scores of its words and the bias, then check the sign. Words that carry strong opinions end up with large weights, and neutral words end up with weights close to zero.</p><p>One last thing to keep in mind is that this setup isn&#8217;t just for reviews. The features can be anything you can count or measure, like the symptoms of a patient or an email&#8217;s properties. As long as each feature has a weight, the perceptron works the same way.</p><h2>How Good Is a Classifier? The Perceptron Error</h2><p>So far we have figured out the weights and the bias by looking at the data. That works for 4 reviews and 2 words, but not for millions of reviews and an entire dictionary. We want the machine to find them, so we need to teach the machine how to distinguish a good classifier from a bad one. In the linear regression article, we did this with an error function: a value that is large when the model performs badly and small when it performs well. We need the same thing here, and it turns out that building it takes three attempts.</p>
      <p>
          <a href="https://newsletter.francofernando.com/p/the-perceptron">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[How the Operating System Runs Many Tasks on One CPU]]></title><description><![CDATA[Multitasking, context switching, and the scheduler that makes it all work.]]></description><link>https://newsletter.francofernando.com/p/how-the-operating-system-runs-many</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/how-the-operating-system-runs-many</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 11 Jul 2026 08:25:54 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!TQSb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36d7503e-7528-4ff7-bcc1-afbf6bbc3005_1050x429.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Welcome to the 181st issue of the Polymathic Engineer newsletter.</p><p>Take a moment and think about what your computer is doing right now. You have a browser open with a dozen tabs, a music player running in the background, a chat app, and the editor you are typing in. Everything is going on together, without any hitch. We take it for granted, but it is a remarkable thing.</p><p>Most of the time, all this is happening on a machine that can really only physically run a handful of things at a time, and just one thing at a time on a single core. So how can the computer keep these jobs going at once? And what is going on underneath when it seems as if everything is running in parallel?</p><p>In a recent article, we broke down concurrent programs into processes and threads, and started calling them tasks. Now we look at the layer that brings those tasks to life. The operating system takes many tasks that all want the CPU and juggles them so fast that it feels like everything is running at once. This trick is called multitasking, and it is the reason a single core can feel like many.</p><p>The outline is as follows:</p><ul><li><p>CPU-bound and I/O-bound tasks</p></li><li><p>Why one core isn&#8217;t enough</p></li><li><p>Preemptive multitasking</p></li><li><p>Context switching</p></li><li><p>The scheduler</p></li><li><p>Multitasking beyond the operating system</p></li></ul><div><hr></div><p><span>Project-based learning is the best way to develop technical skills. </span><a href="https://app.codecrafters.io/join?via=FrancoFernando">CodeCrafters </a><span>is an excellent platform for tackling exciting projects, such as building your own Redis, Kafka, a </span><a href="https://app.codecrafters.io/join/dns-server?via=francofernando">DNS server</a><span>, SQLite, an HTTP server, or Git from scratch using your favorite programming language. Now you can also try to build your own Claude Code (for free, still in beta).</span></p><p><a href="https://app.codecrafters.io/join?via=FrancoFernando">Sign up and become a better software engineer</a><span>.</span></p><div><hr></div><h2>CPU-bound and I/O-bound tasks</h2><p>Before we get to how the operating system juggles tasks, we need to look at what kind of work those tasks actually do. Not all work is the same, and the difference matters a lot for how the OS treats it.</p><p>Every application does two broad kinds of work. The first is computation: arithmetic, logic, comparisons, all the number crunching the CPU is built to. The second is input and output (I/O): reading from the keyboard, the disk, or the network, drawing on the screen, writing files, or sending data over a connection. I/O is really just the process of sending and receiving signals to devices. And most of the time, it does not involve the CPU at all. There is nothing to compute. We are just waiting for the device to answer.</p><p>This provides us with a simple way to classify tasks. We say a task is bound by some resource when that resource is the bottleneck: the thing that, if it were faster, would make the whole task faster. There are two main kinds.</p><p>A task is CPU-bound if it would run faster with a faster CPU. It spends most of its time computing. Some typical examples are:</p><ul><li><p>Math like additions, divisions, and matrix multiplication</p></li><li><p>Encryption and decryption, which lean on heavy operations like prime factorization</p></li><li><p>Image and video processing</p></li><li><p>Running algorithms like sorting and binary search</p></li></ul><p>A task is I/O-bound if it would run faster with a quicker I/O subsystem, whether that means reading from disk, getting user input, or waiting on a network response. Here the CPU often does nothing but wait for data to move to or from some device, and the CPU time is expensive. Some typical examples are:</p><ul><li><p>Most apps with a graphical interface, even if they never touch the disk, since they spend their time waiting on the keyboard or the mouse</p></li><li><p>Databases and web servers, which spend most of their time on disk or network I/O</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TQSb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36d7503e-7528-4ff7-bcc1-afbf6bbc3005_1050x429.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TQSb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36d7503e-7528-4ff7-bcc1-afbf6bbc3005_1050x429.png 424w, https://substackcdn.com/image/fetch/$s_!TQSb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36d7503e-7528-4ff7-bcc1-afbf6bbc3005_1050x429.png 848w, https://substackcdn.com/image/fetch/$s_!TQSb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36d7503e-7528-4ff7-bcc1-afbf6bbc3005_1050x429.png 1272w, https://substackcdn.com/image/fetch/$s_!TQSb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36d7503e-7528-4ff7-bcc1-afbf6bbc3005_1050x429.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TQSb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36d7503e-7528-4ff7-bcc1-afbf6bbc3005_1050x429.png" width="606" height="247.59428571428572" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/36d7503e-7528-4ff7-bcc1-afbf6bbc3005_1050x429.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:429,&quot;width&quot;:1050,&quot;resizeWidth&quot;:606,&quot;bytes&quot;:151716,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/206030637?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36d7503e-7528-4ff7-bcc1-afbf6bbc3005_1050x429.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!TQSb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36d7503e-7528-4ff7-bcc1-afbf6bbc3005_1050x429.png 424w, https://substackcdn.com/image/fetch/$s_!TQSb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36d7503e-7528-4ff7-bcc1-afbf6bbc3005_1050x429.png 848w, https://substackcdn.com/image/fetch/$s_!TQSb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36d7503e-7528-4ff7-bcc1-afbf6bbc3005_1050x429.png 1272w, https://substackcdn.com/image/fetch/$s_!TQSb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36d7503e-7528-4ff7-bcc1-afbf6bbc3005_1050x429.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Why this distinction matters? Because it tells us what resource to improve. Take two programs. The first multiplies two huge matrices and returns the answer. The second copies a huge amount of data from the network onto a disk. These two will not speed up in the same way. What good is a faster clock, or a thousand cores instead of one, if the computer spends most of its time waiting for the next batch of data to reach the disk? An I/O bound load will not get better with more cores. However a CPU bound load can really benefit from being spread out across many cores at once.</p><p>There is one more thing to know. Over the years, applications have become more and more I/O-bound. The CPU speed kept increasing, and let us run more instructions in the same amount of time, but the data transfer speed never caught up. So the limiting factor in a program is frequently some I/O operation that blocks the CPU. The good news is that these blocking tasks can be detected and forced to run in the background, and that is precisely what modern runtime systems do. That background juggling is where multitasking begins.</p><h2>Why one core isn&#8217;t enough</h2><p>Let&#8217;s make this more concrete. Suppose one of your friends pulls an old portable game console out of a drawer. The device has one slow core, a little pixelated screen, and a few buttons. Your friend knows you write code, so she asks you to build a simple maze game for it, where a hero runs around collecting coins while monsters chase it.</p><p>Think about all the things the game has to do. It has to read the buttons so the player can move the hero. At the same time, the world keeps changing on its own, with the monsters moving and the coins being tracked. And the player has to see all of this on the screen as it happens.</p><p>So you split the work into three jobs:</p><ul><li><p>Reading the player&#8217;s input from the buttons. This is I/O-bound, since most of the time it just waits for a press.</p></li><li><p>Computing the next state of the world from the game rules and the player&#8217;s moves. This is CPU-bound.</p></li><li><p>Drawing the updated world on the screen. This is I/O-bound again.</p></li></ul><p>The problem is that all three jobs need to make progress together, or the game feels broken. If the console stops reading input while it redraws the screen, the controls freeze. But you have a single core, and it can only ever run one job at a time.</p><p>Your first instinct might be to pick up threads. You spawn a single process with three threads, one for each of the jobs, and let them share the game state. It is a reasonable idea. However, if you run it, then the software hangs in the first thread. Reading input is an unending cycle of its own, waiting for a button, never letting the other two run. The single core has room for one thread, and that thread is hogging it.</p><p>The lesson is that threads alone don&#8217;t buy you anything here. Running things truly at the same instant, which is what we call parallelism, needs hardware with more than one core. On a single core, no amount of threads will make two jobs run at the same moment.</p><p>So how do we get out of this? The way out comes from separating two ideas that are easy to mix up: concurrency and parallelism. Parallelism is when things literally run at the same instant on different cores. Concurrency is less powerful but more general. It means that numerous jobs have overlapping lifetimes: they are all in progress over the same span of time, even though at any given instant only one of them is running.</p><p>This distinction is the whole point. On one core, we can&#8217;t run the three gaming jobs in parallel<span>. But we can run them </span>concurrently, switching between them fast enough that they all make progress. To the player, it looks like everything is moving together. That act of switching is multitasking, and it is how a single core fakes doing multiple things at once.</p><p>There is a nice way to think about the relationship between the two. Parallelism is an implementation detail of how the work physically runs. Concurrency is part of how we design the program in the first place. With the right hardware, the OS can turn our concurrent design into real parallelism. Without it, the OS still honors the design by interleaving the jobs on one core. Either way, from our point of view as programmers, the jobs run concurrently.</p><h2>Preemptive multitasking</h2>
      <p>
          <a href="https://newsletter.francofernando.com/p/how-the-operating-system-runs-many">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[When to repeat yourself ]]></title><description><![CDATA[When duplicated code is the better tradeoff.]]></description><link>https://newsletter.francofernando.com/p/when-to-repeat-yourself</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/when-to-repeat-yourself</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 04 Jul 2026 06:05:31 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!anDV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf57c2b7-9d6d-48f8-b119-0a3cf4534bb8_1869x592.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Welcome to the 180th issue of the Polymathic Engineer newsletter.</p><p><a href="https://newsletter.francofernando.com/p/the-dry-principle">In a recent article</a>, we talked about the DRY principle and why removing duplication can lead to fewer bugs and more reusable code. As you know, DRY is one of the most well-known rules in software engineering, and most of us learn it early in our careers.</p><p>But here&#8217;s the thing: DRY is not free. Every time we remove duplication, we pay for it somewhere else. Sometimes it is the coordination between teams. Sometimes it is the tight coupling between components. Sometimes a new business requirement arrives, our common abstraction is not flexible enough to support it and starts to push back. </p><p>In a small codebase owned by a single team, this cost is small, and DRY almost always wins. In distributed systems with many services and many teams, the cost can become larger than the duplication it is trying to remove.</p><p>There is also a second question that engineers tend to skip. Two sections of code that appear the same may not mean the same thing. Sometimes what looks like duplication is just two things that look alike today and may evolve differently tomorrow, </p><p>In this issue, we will see when keeping duplication is a reasonable tradeoff, and when it is worth the effort to remove it. The outline is as follows:</p><ul><li><p>The coordination tax</p></li><li><p>Is it really duplication?</p></li><li><p>Sharing code across services: from library to microservice</p></li><li><p>The same tradeoff inside a codebase</p></li><li><p>Closing thoughts</p></li></ul><div><hr></div><h4><strong><a href="https://www.sonarsource.com/solutions/reduce-technical-debt/?utm_source=fnf&amp;utm_medium=paid&amp;utm_campaign=ss-techdebt26&amp;utm_term=newsletter-ffranco">Quantify and Eliminate the &#8220;Friction Tax&#8221; on Your Codebase</a></strong></h4><p>Technical debt isn&#8217;t just an abstract problem. As AI accelerates code generation, it also compounds debt, acting as a silent tax on your engineering velocity and exponentially increasing developer cognitive load.</p><p>SonarQube gives tech leaders and architects the exact visibility they need to stop software entropy in its tracks. By analyzing your codebase against thousands of language-specific rules, SonarQube acts as an automated, multi-layered guardrail that keeps your code predictable, adaptable, and architecturally sound.</p><p><strong><a href="https://www.sonarsource.com/solutions/reduce-technical-debt/?utm_source=fnf&amp;utm_medium=paid&amp;utm_campaign=ss-techdebt26&amp;utm_term=newsletter-ffranco">Discover how to systematically measure and manage code-level debt with SonarQube.</a></strong></p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ROkk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd75bf2a9-3b98-4055-9642-813601cfb439_320x114.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ROkk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd75bf2a9-3b98-4055-9642-813601cfb439_320x114.png 424w, https://substackcdn.com/image/fetch/$s_!ROkk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd75bf2a9-3b98-4055-9642-813601cfb439_320x114.png 848w, https://substackcdn.com/image/fetch/$s_!ROkk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd75bf2a9-3b98-4055-9642-813601cfb439_320x114.png 1272w, https://substackcdn.com/image/fetch/$s_!ROkk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd75bf2a9-3b98-4055-9642-813601cfb439_320x114.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ROkk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd75bf2a9-3b98-4055-9642-813601cfb439_320x114.png" width="240" height="85.5" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d75bf2a9-3b98-4055-9642-813601cfb439_320x114.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:114,&quot;width&quot;:320,&quot;resizeWidth&quot;:240,&quot;bytes&quot;:25553,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/197145117?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd75bf2a9-3b98-4055-9642-813601cfb439_320x114.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ROkk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd75bf2a9-3b98-4055-9642-813601cfb439_320x114.png 424w, https://substackcdn.com/image/fetch/$s_!ROkk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd75bf2a9-3b98-4055-9642-813601cfb439_320x114.png 848w, https://substackcdn.com/image/fetch/$s_!ROkk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd75bf2a9-3b98-4055-9642-813601cfb439_320x114.png 1272w, https://substackcdn.com/image/fetch/$s_!ROkk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd75bf2a9-3b98-4055-9642-813601cfb439_320x114.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Thanks to the<strong> </strong>SonarQube Team for collaborating with me on this article.</p><div><hr></div><h2>The coordination tax</h2><p>To see why duplication is sometimes the better tradeoff, let&#8217;s suppose that two teams are building two services that need a similar piece of functionality. Team A writes its version in its own service, and Team B does the same. The two sections of code look almost identical, but they are in different repos and are owned by different teams.</p><p>This is the classic scenario where most engineers reach for DRY and say: it should be a shared component. On the surface, that sounds right. Two teams writing the same thing twice is exactly what DRY warns us about.</p><p>However, there is a hidden benefit to leaving the duplication in place. Both teams can move at full speed. There are no meetings to align on the design, no shared roadmap, no review process across team boundaries, and no merge conflicts on someone else&#8217;s repository. Each team owns its copy and can change it whenever it wants.</p><p>The parts of a project that need everybody in the room don&#8217;t get faster when you add more people. They slow down. The cost of sharing is in the meetings, design reviews, sign-offs, and merge negotiations. When you remove duplication by adding a shared component, you also introduce coordination among everyone who uses it.</p><p>Of course, the other side of the coin is real too. When each team owns its own copy, a bug fixed in one area doesn&#8217;t make it to the other. Knowledge doesn&#8217;t spread. The two implementations slowly drift apart, and after a few months, they may not even behave the same way for the same input. This is particularly risky for security-sensitive code because the likelihood of two independent teams getting authentication or encryption right twice is quite low.</p><p>So the question is not if we should remove duplication, but what is more expensive in our situation. Which one creates more trouble: the coordination cost of sharing, or the drift cost of duplicating? The answer is different for each team and depends on how often the code changes and how different the two versions are going to be.</p><h2>Is it really duplication?</h2><p>Before we think about how to share code, there is another crucial question to answer first: is what we are looking at really duplication?</p><p>The fact that two sections of code look very similar does not mean they solve the same problem. Two validation functions that both check &#8220;value greater than zero&#8221; can look the same on the screen, but they might have different purposes. For example, the first might be validating the user&#8217;s age and the second a product price. The validation rules are the same now, but they may evolve differently tomorrow.</p><p>This is what we call incidental duplication. It is code that looks the same but does not encode the same knowledge. It is different from inherent duplication, where the same business rule is represented in multiple places. Inherent duplication is something we usually want to remove. Incidental duplication is something we usually want to leave alone.</p><p>The tricky thing is that it can be hard to distinguish the two scenarios, especially early in a project. What appears to be duplicated code today could turn out to belong to two different domains tomorrow. Here are some hints to help: </p><ul><li><p>The two call sites are owed by different teams or driven by different stakeholders</p></li><li><p>The reason for the logic is different on each side, even though the code looks identical</p></li><li><p>You can imagine a future where one side changes and the other does not</p></li></ul><p>When any of these signals are present, it is usually better to let the duplication live for a while. If there is a pattern, it will become clear over time, and the proper abstraction will emerge from real evidence rather than a guess. But if the pattern never solidifies, you have lost nothing.</p><p>There is also an asymmetry that makes this more than a suggestion. Once you create a shared abstraction, every caller is coupled to it. As new callers are added, the coupling becomes stronger since any change needs to satisfy all of them. Splitting it back apart once there are a dozen consumers is hard, sometimes harder than the duplication you were trying to remove. Going the other way is much easier. Two similar things can be merged into one whenever you decide they really are the same.</p><p>When you are in doubt, it is better to keep the duplication. The price of being patient is small. Premature abstraction is expensive, paid over many years by many engineers in code that no one fully knows how to disentangle.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.francofernando.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Polymathic Engineer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Sharing code across services: from library to microservice</h2><p>Let&#8217;s make this concrete with a real-world example. Suppose we have two services in the system: payments and notifications. Both of them expose a public API, and both need to throttle clients to protect themselves from abuse. Team payments writes its own rate limiter inside the payments service. Team notifications does the same.</p><p>After a few months, the duplication becomes a pain. A bug in the token bucket logic gets fixed on the payments side, but it is not applied on the notifications side. At this point, someone suggests the obvious next step: let&#8217;s put the rate-limiting code into a shared component. The question is: what shape should that component take?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!anDV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf57c2b7-9d6d-48f8-b119-0a3cf4534bb8_1869x592.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!anDV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf57c2b7-9d6d-48f8-b119-0a3cf4534bb8_1869x592.png 424w, https://substackcdn.com/image/fetch/$s_!anDV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf57c2b7-9d6d-48f8-b119-0a3cf4534bb8_1869x592.png 848w, https://substackcdn.com/image/fetch/$s_!anDV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf57c2b7-9d6d-48f8-b119-0a3cf4534bb8_1869x592.png 1272w, https://substackcdn.com/image/fetch/$s_!anDV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf57c2b7-9d6d-48f8-b119-0a3cf4534bb8_1869x592.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!anDV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf57c2b7-9d6d-48f8-b119-0a3cf4534bb8_1869x592.png" width="1456" height="461" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/df57c2b7-9d6d-48f8-b119-0a3cf4534bb8_1869x592.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:461,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:348316,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/197145117?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf57c2b7-9d6d-48f8-b119-0a3cf4534bb8_1869x592.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!anDV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf57c2b7-9d6d-48f8-b119-0a3cf4534bb8_1869x592.png 424w, https://substackcdn.com/image/fetch/$s_!anDV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf57c2b7-9d6d-48f8-b119-0a3cf4534bb8_1869x592.png 848w, https://substackcdn.com/image/fetch/$s_!anDV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf57c2b7-9d6d-48f8-b119-0a3cf4534bb8_1869x592.png 1272w, https://substackcdn.com/image/fetch/$s_!anDV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf57c2b7-9d6d-48f8-b119-0a3cf4534bb8_1869x592.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Option one is a library. Both services pull it in as a dependency. The rate limiter itself is in its own repo. This is the cheapest path. The fixed cost of setting up the library is small: a build, a versioning scheme, an artifact repository, and some documentation. Once it is in place, bug fixes are available to all clients at the next release. Both teams work on the same codebase, and knowledge spreads naturally.</p><p>However, a library is not a black box. When you import it, its code is included in your service, with all the consequences that follow. The first is the programming language. If payments is written in Go and notifications in Java, a shared library is not going to work. The second one is transitive dependencies. Your rate-limiting library pulls in a Redis client at version 4. The payments service uses another library that requires the Redis client to be version 6. Now everyone fights about which version wins, and the resolution rules of your build tool tell you which one to use. Debugging such conflicts can be tricky and may even turn off other teams from adopting your library in the first place. The right defensive move is to keep direct dependencies to a minimum, but the issue never really goes away.</p><p>In the end, libraries exchange duplication for coupling on the dependency level. The two services cannot evolve independently, but must agree on the language stack and tolerate each other&#8217;s transitive dependencies.</p><p>The second option is to extract rate limiting as its own microservice. Both clients now invoke an HTTP (or a gRPC) endpoint rather than importing a package. The boundary between services is an API, which is a cleaner contract than a binary dependency. The rate-limiting service can be deployed and scaled on its own. The team that owns it is free to change the implementation, as long as the API stays compatible. API evolution is also easier because you can measure endpoint usage and deprecate the ones nobody calls anymore.</p><p>However, this approach is more expensive. Each request for payments or notifications now makes an extra network call to the rate-limiting service. Latency budgets become tight, and you may need caching, retries, or speculative execution to meet your service-level agreements. If the dependency breaks, well, that is also your problem now. What should payments do if the rate-limiting service is down? Remain safe and decline all incoming requests, or accept them all and stay available?</p><p>Both decisions have implications, and you need to choose thoughtfully. On top of that, running a service has operational costs such as monitoring, on-call rotations, alerting, and capacity planning. The first one in particular is a serious investment.</p><p>To wrap up the comparison, a library is cheap and tightly coupled at the dependency level. A microservice is expensive and loosely coupled at the API level. There isn&#8217;t a universal answer. The choice depends on how complex the shared logic is, whether it really has its own business domain, what your latency requirements are, and whether your organization can afford another running service. The simpler the logic, the more likely a library is to be enough. The more independent and complex the domain is, the more a microservice starts to make sense.</p><h2>The same tradeoff inside a codebase</h2><p>The same tradeoff applies one step down. The standard mechanism for sharing code inside a single codebase is inheritance. You extract the common functionalities into a base class and let subclasses provide the specialized behavior. This is clean in theory, but has the same coupling cost as a shared library or shared service, just on a smaller scale.</p><p>Let&#8217;s stick with the notifications service from the previous section. Suppose it has to send messages over two different channels: email and SMS. Both channels need the same infrastructure: a buffer for outgoing messages, logic for retrying messages that fail temporarily, and batching for when traffic is high. The call that sends the message to the provider is the only real difference between them.</p><p>The textbook solution is to extract a BaseNotificationSender class with the buffering, retry, and batching logic, and let EmailSender and SmsSender extend it by adding only the send method. DRY is satisfied. Both classes look small and easy to read. Everyone is happy until the next change arrives.</p><p>Say marketing wants to run a large campaign and asks that SMS switch to unbounded buffering for the duration. Now we have an issue. The buffering logic is in the parent class, and the parent class is shared with email. We can&#8217;t change the behavior for SMS without changing it for email too, and email has good reasons to preserve its bounded buffer.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PFfI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda05e2a6-730f-4aa3-9d23-ccbd3d9f45ee_1862x462.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PFfI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda05e2a6-730f-4aa3-9d23-ccbd3d9f45ee_1862x462.png 424w, https://substackcdn.com/image/fetch/$s_!PFfI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda05e2a6-730f-4aa3-9d23-ccbd3d9f45ee_1862x462.png 848w, https://substackcdn.com/image/fetch/$s_!PFfI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda05e2a6-730f-4aa3-9d23-ccbd3d9f45ee_1862x462.png 1272w, https://substackcdn.com/image/fetch/$s_!PFfI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda05e2a6-730f-4aa3-9d23-ccbd3d9f45ee_1862x462.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PFfI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda05e2a6-730f-4aa3-9d23-ccbd3d9f45ee_1862x462.png" width="1456" height="361" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/da05e2a6-730f-4aa3-9d23-ccbd3d9f45ee_1862x462.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:361,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:334505,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/197145117?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda05e2a6-730f-4aa3-9d23-ccbd3d9f45ee_1862x462.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!PFfI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda05e2a6-730f-4aa3-9d23-ccbd3d9f45ee_1862x462.png 424w, https://substackcdn.com/image/fetch/$s_!PFfI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda05e2a6-730f-4aa3-9d23-ccbd3d9f45ee_1862x462.png 848w, https://substackcdn.com/image/fetch/$s_!PFfI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda05e2a6-730f-4aa3-9d23-ccbd3d9f45ee_1862x462.png 1272w, https://substackcdn.com/image/fetch/$s_!PFfI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda05e2a6-730f-4aa3-9d23-ccbd3d9f45ee_1862x462.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The instinct is to treat it as an exceptional situation. You could add an if statement in the parent class to check whether it is an instance of SmsSender, or pass a flag, or add a hook method. All these ways work, in the sense that they make the code compile and the campaign ship. But they also leak SMS-specific knowledge into the parent class, and the parent class no longer represents a clean abstraction. It is now a place where everyone&#8217;s special cases live. The next time a divergent change comes in for email, we do the same thing again. After a few iterations of this, the parent class gets harder to understand than the two duplicated subclasses would have been in the first place.</p><p>The takeaway is that inheritance is shared code with extra coupling. The moment one child needs to diverge, the abstraction starts to push back.</p><p>However, there is another approach to deal with this, which is composition. Instead of one inheritance hierarchy, you split the responsibilities into distinct pieces: a buffer, a retry policy, and a transport. Then you plumb them together. The email sender uses a bounded buffer with HTTP transport. The SMS sender uses an unbounded buffer and a different transport. The buffer is no longer coupled to the transport. The flexibility is real, and so is the cost: there are more moving parts to understand, and the reader has to hold more pieces in their head to follow what happens when a message is sent.</p><p>There is no free lunch. Inheritance is cheap and rigid. Composition is flexible but has more moving parts. The best choice really depends on the divergence you expect.</p><h2>Closing thoughts</h2><p>DRY is not wrong. It is just not free. If we remove duplication, the risk is that we pay for it somewhere else. These costs can be in the form of coordination issues between teams, dependency problems, extra network calls, or abstractions that get in the way when a new change comes in.</p><p>The skill is being able to tell when the coordination cost of sharing is higher than the maintenance cost of duplicating. And that calculation shifts as you cross boundaries. </p><p>Inside a small codebase owned by one team, DRY almost always wins. But across team boundaries, service boundaries, or tech stacks, the math gets less straightforward, and duplication can be the better answer.</p><p>There is one more thing worth keeping in mind. It&#8217;s much easier to merge two similar things later than to split a shared abstraction once it has a dozen callers depending on it. Going from duplicated to shared is easy to revert. Going from shared to duplicated is hard, sometimes harder than living with the duplication in the first place.</p><p>So when in doubt, let the duplication live. The right abstraction will emerge from real evidence, or it will not. Either way, you are better off than committing to the wrong one too early.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.francofernando.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Polymathic Engineer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[The Training Trap: Underfitting, Overfitting, and How to Escape Them]]></title><description><![CDATA[A practical guide to the most common problems in machine learning training and the techniques to solve them.]]></description><link>https://newsletter.francofernando.com/p/the-training-trap-underfitting-overfitting</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/the-training-trap-underfitting-overfitting</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 27 Jun 2026 08:34:14 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!XJhX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa9c0da-ac82-4673-9964-82ca7d6632d1_1366x533.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Welcome to the 179th issue of the Polymathic Engineer newsletter. </p><p>Regardless of whether you are an experienced machine learning practitioner or just a data scientist who has started their journey, you have surely had the same frustrating experience. You built a model, trained it on your data, everything looked fine, but then it fell apart in production. </p><p>There are two issues to be aware of. The first one is called <strong>underfitting</strong>. A model that underfits is too simple for the data. It makes many mistakes, even on the training data itself, because it can't capture the patterns underneath.</p><p>The second problem is called <strong>overfitting</strong>. This is the opposite of the first problem. In this situation, the model memorizes the training set perfectly; therefore, it can&#8217;t make generalizations.</p><p>In this article, we explore both underfitting and overfitting, as well as the methods to detect and address them. The outline is as follows:</p><ul><li><p>What can go wrong when training a model?</p></li><li><p>Underfitting and overfitting: two sides of the same coin</p></li><li><p>Detecting the problem with testing</p></li><li><p>The golden rule and the validation set</p></li><li><p>The model complexity graph</p></li><li><p>Solving the problem: regularization</p></li><li><p>Measuring complexity: L1 and L2 norms</p></li><li><p>The regularization parameter</p></li><li><p>When to use L1 vs L2</p></li></ul><div><hr></div><p><span>Project-based learning is the best way to develop technical skills. </span><a href="https://app.codecrafters.io/join?via=FrancoFernando">CodeCrafters </a><span>is an excellent platform for tackling exciting projects, such as building your own Redis, Kafka, a </span><a href="https://app.codecrafters.io/join/dns-server?via=francofernando">DNS server</a><span>, SQLite, an HTTP server, or Git from scratch using your favorite programming language. Now you can also try to build your own Claude Code (for free, still in beta).</span></p><p><a href="https://app.codecrafters.io/join?via=FrancoFernando">Sign up, and become a better software engineer</a><span>.</span></p><div><hr></div><h2>What can go wrong when training a model?</h2><p>Let&#8217;s try to answer this question starting with a simple analogy. Imagine you have to study for an exam. There are many things that could go wrong as you prepare. </p><p>You might not have studied enough. There is no way to fix that, and you are likely to perform poorly. But what if you studied a lot, just in the wrong way? For example, you choose to memorize the entire textbook word for word instead of trying to understand the material. Will you do well? Likely not, because you memorized everything without actually learning anything. You won&#8217;t know how to answer questions on the exam that you haven&#8217;t seen before. </p><p>The best approach is to study in a way that lets you answer questions you haven&#8217;t seen during your preparation.</p><p>The same things can go wrong with machine learning models. Not studying enough is like underfitting: the model is too simplistic to learn anything helpful from the input data. Memorizing the textbook is like overfitting: the model becomes too complicated and remembers the training data rather than learning the underlying patterns. A good model is one that learns the data properly and can make good predictions on new data it has never seen.</p><p>There is a fundamental trade-off in machine learning between finding the best model for the training data and ensuring it performs well with unseen data. <strong>Optimization</strong> is the process of tuning a model to work as well as possible on the training data. On the other hand, <strong>generalization</strong> is how well the model performs with data it has never seen before. The goal is always to generalize well, but there is a catch: you can only control the optimization. You adjust the model to fit the training data, and you hope it works well on other data. If you optimize too much, you can overfit, and generalization gets worse.</p><p>In the next section, we will look at underfitting and overfitting in more detail and see what they look like in practice.</p><h2>Underfitting and overfitting: two sides of the same coin</h2><p>A different way to look at underfitting and overfitting is through the lens of problem- solving. There are two mistakes you can make when you have a problem to solve. You can make the problem too simple and come up with an overly basic solution. Or you can overcomplicate the problem and come up with a solution that is too elaborate.</p><p>Imagine your task is to fix a leaking faucet. If you just put a bucket under it, you have oversimplified the problem. The faucet keeps dripping, and you are only catching the water, not fixing anything. This is underfitting: you are trying to model your data with something that is too simple to work effectively.</p><p>On the other hand, if you knock out all the plumbing in your house to fix a small drip, you have overcomplicated the solution. You may have fixed the leak, but this approach wasted resources and made the work more difficult than necessary. This is overfitting: your data might be simple, but you try to fit it with a model that is too complex. The model will fit the training data, but it will memorize it rather than learn from it.</p><p>Let&#8217;s look at a concrete example with polynomial regression. Suppose that you have a dataset that looks like a parabola, which is a curve that goes up and then down. You want to fit a polynomial to this data, but you don&#8217;t know what degree. Should you use a line, a quadratic, or something more complex?</p><p>If you fit a line (a polynomial of degree 1), the model is too simple. It is impossible for a straight line to capture the shape of a parabola, and the model underfits. If you fit a quadratic (a degree-2 polynomial), the model captures the essence of the data well. It neither underfits nor overfits. However, if you fit a polynomial of degree 10, the model becomes too flexible. It can bend and twist to reach each point in the dataset, but that is not the goal. Instead of learning that the shape looks like a parabola, it memorizes the exact positions of each point. The model overfits.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!XJhX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa9c0da-ac82-4673-9964-82ca7d6632d1_1366x533.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!XJhX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa9c0da-ac82-4673-9964-82ca7d6632d1_1366x533.png 424w, https://substackcdn.com/image/fetch/$s_!XJhX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa9c0da-ac82-4673-9964-82ca7d6632d1_1366x533.png 848w, https://substackcdn.com/image/fetch/$s_!XJhX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa9c0da-ac82-4673-9964-82ca7d6632d1_1366x533.png 1272w, https://substackcdn.com/image/fetch/$s_!XJhX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa9c0da-ac82-4673-9964-82ca7d6632d1_1366x533.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!XJhX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa9c0da-ac82-4673-9964-82ca7d6632d1_1366x533.png" width="1366" height="533" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/afa9c0da-ac82-4673-9964-82ca7d6632d1_1366x533.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:533,&quot;width&quot;:1366,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:74469,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/194885882?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa9c0da-ac82-4673-9964-82ca7d6632d1_1366x533.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!XJhX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa9c0da-ac82-4673-9964-82ca7d6632d1_1366x533.png 424w, https://substackcdn.com/image/fetch/$s_!XJhX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa9c0da-ac82-4673-9964-82ca7d6632d1_1366x533.png 848w, https://substackcdn.com/image/fetch/$s_!XJhX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa9c0da-ac82-4673-9964-82ca7d6632d1_1366x533.png 1272w, https://substackcdn.com/image/fetch/$s_!XJhX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa9c0da-ac82-4673-9964-82ca7d6632d1_1366x533.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The real problem with overfitting shows up when you try to make predictions on new data. A model that memorized the training set will perform very poorly on data it has never seen before. It learned the noise and quirks in the training data, rather than the underlying pattern. Here is a simple way to tell what is happening with your model:</p><ul><li><p><strong>Underfitting:</strong> The model performs poorly on the training data and poorly on new data. It is too simple to learn anything useful.</p></li><li><p><strong>Good fit:</strong> The model performs well on the training data and on new data. It has learned the underlying patterns.</p></li><li><p><strong>Overfitting:</strong> The model performs very well on the training data but poorly on new data. It has memorized rather than learned.</p></li></ul><p>But how can you actually measure this? How can you know if your model will perform well on new data before you deploy it? This is where testing and validation come in.</p>
      <p>
          <a href="https://newsletter.francofernando.com/p/the-training-trap-underfitting-overfitting">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Service Mesh vs API Gateway (Part III)]]></title><description><![CDATA[Two patterns that look similar, solve different problems, and work best together.]]></description><link>https://newsletter.francofernando.com/p/service-mesh-vs-api-gateway-part</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/service-mesh-vs-api-gateway-part</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 20 Jun 2026 09:02:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!NfUp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd493ef15-b51f-4f27-9e25-ed73ad56141a_1143x670.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Welcome to the 178th issue of the Polymathic Engineer newsletter.</p><p>This week, we are wrapping up our series of articles on service mesh. In the <a href="https://newsletter.francofernando.com/p/service-mesh-why-and-how-it-works">first post</a>, we talked about what a service mesh is and how its architecture works. <a href="https://newsletter.francofernando.com/p/what-a-service-mesh-gives-you-part">In the second</a>, we looked at what it actually gives you in practice and at the trade-offs that come with it.</p><p>There is one more topic that feels at home in this series, and often confuses folks: the difference between a service mesh and an API gateway. Both manage traffic, both can do things like authentication, routing, and observability, and sit somewhere between a client and a service. It&#8217;s easy to think of both as the same kind of tool, or to reduce the difference to a simple rule of thumb such as &#8220;API gateways are for north-south traffic, service meshes are for east-west,&#8221; and go on.</p><p>That shortcut is a one-liner, but it masks what each tool is actually good at. They were built to tackle different problems, and treating them as interchangeable usually leads to one of two architectural mistakes: forcing one tool to do the other&#8217;s job, or layering them together without a clear understanding of why.</p><p>In this issue, we look at API gateways and service meshes side by side. We will discuss what an API gateway does, how it differs from a service mesh, when to use which, and how they work together when used correctly.</p><p>The outline is as follows:</p><ul><li><p>What an API gateway does</p></li><li><p>Side-by-side differences</p></li><li><p>When to use which</p></li><li><p>Using them together</p></li><li><p>Wrapping up the series</p></li></ul><div><hr></div><p>Project-based learning is the best way to develop technical skills. <a href="https://app.codecrafters.io/join?via=FrancoFernando">CodeCrafters </a>is an excellent platform for tackling exciting projects, such as building your own Redis, Kafka, a <a href="https://app.codecrafters.io/join/dns-server?via=francofernando">DNS server</a>, SQLite, an HTTP server, or Git from scratch using your favorite programming language. Now you can also try to build your own Claude Code (for free, still in beta).</p><p><a href="https://app.codecrafters.io/join?via=FrancoFernando">Sign up, and become a better software engineer</a>.</p><div><hr></div><h2>What an API gateway does</h2><p>An API gateway lives at the edge of your system and is the front door for everything coming in from outside. Mobile apps, web browsers, partner systems, and any other external client send their requests to the gateway, and the gateway is responsible for handing them off to the right backend service. It is a centralized component, which means that a single instance, or a small cluster, takes care of all the traffic entering the system.</p><p>This central position is what makes the gateway so useful. Each external request flows through it, so you can put a lot of cross-cutting work in one place instead of spreading it across many services. The gateway works at the application layer of the OSI model. This means that it understands HTTP, HTTPS, gRPC, and WebSocket, and it can read URLs, headers, methods, and payloads to decide what to do with each request.</p><p>The most basic job of an API gateway is routing. It looks at the path or the method of an incoming request and forwards it to the right backend service. It can also translate between protocols: a common example is accepting an HTTP call from a mobile client and turning it into a gRPC call to an internal service.</p><p>Another big responsibility is authentication and authorization. The API gateway does validation once at the edge, instead of each backend service having to include its own logic. It can validate OAuth2 tokens, JWTs, API keys, and integrate with an identity provider. The requests that do not pass these checks are rejected before they reach the internal services. </p><p>The gateway also takes care of rate limiting and request shaping. It is able to throttle abusive clients, block requests that aren&#8217;t valid, and stop a single noisy customer from taking over the whole system. This keeps bad behavior on the outside from affecting services in the backend.</p><p>Some other common tasks are spreading the load across multiple backend instances, caching frequent responses to speed up the system, or gathering logs and metrics for the traffic that goes through. Since the gateway is the single point through which all external traffic passes, it is a natural place to gather observability data on how clients are using the APIs.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NfUp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd493ef15-b51f-4f27-9e25-ed73ad56141a_1143x670.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NfUp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd493ef15-b51f-4f27-9e25-ed73ad56141a_1143x670.png 424w, https://substackcdn.com/image/fetch/$s_!NfUp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd493ef15-b51f-4f27-9e25-ed73ad56141a_1143x670.png 848w, https://substackcdn.com/image/fetch/$s_!NfUp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd493ef15-b51f-4f27-9e25-ed73ad56141a_1143x670.png 1272w, https://substackcdn.com/image/fetch/$s_!NfUp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd493ef15-b51f-4f27-9e25-ed73ad56141a_1143x670.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NfUp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd493ef15-b51f-4f27-9e25-ed73ad56141a_1143x670.png" width="523" height="306.57042869641293" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d493ef15-b51f-4f27-9e25-ed73ad56141a_1143x670.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:670,&quot;width&quot;:1143,&quot;resizeWidth&quot;:523,&quot;bytes&quot;:389018,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/195423488?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd493ef15-b51f-4f27-9e25-ed73ad56141a_1143x670.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NfUp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd493ef15-b51f-4f27-9e25-ed73ad56141a_1143x670.png 424w, https://substackcdn.com/image/fetch/$s_!NfUp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd493ef15-b51f-4f27-9e25-ed73ad56141a_1143x670.png 848w, https://substackcdn.com/image/fetch/$s_!NfUp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd493ef15-b51f-4f27-9e25-ed73ad56141a_1143x670.png 1272w, https://substackcdn.com/image/fetch/$s_!NfUp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd493ef15-b51f-4f27-9e25-ed73ad56141a_1143x670.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Many API gateways also handle API versioning. Backend services do change, but the contract that external clients rely on cannot break overnight. The gateway can give a stable interface to customers while sending requests to newer versions of the services behind the scenes. It can also change the response so that older clients get what they expect.</p><p>Besides traffic, API gateways today are typically the key to what is sometimes referred to as API productization. If you think about APIs as products which are consumed by internal teams, partners, or third-party developers, routing and auth are not enough. A lot of gateways are integrated with large API management platforms with developer portals, self-service onboarding, monetization features, sandbox environments, and access control dashboards. This transforms the gateway from a network infrastructure element into the front office of your APIs.</p><p>For a more detailed overview of API gateways, you can refer to <a href="https://newsletter.francofernando.com/p/api-gateways">our previous article</a>.</p><h2>Side-by-side differences</h2><p>Now that we have looked at both an API gateway and a service mesh in isolation, let&#8217;s put them side by side. There are 5 main differences worth knowing about.</p><p>The first one is the deployment model. A gateway is a centralized component. You can have a single instance or a small cluster at the edge that handles all the traffic coming from the outside. That makes it easier to apply global policies and observe all external interactions from a single place.</p><p>On the other hand, a service mesh is decentralized. It uses a sidecar proxy adjacent to each service replica instead of a single component. This shifts traffic control to each service, giving you fine-grained control, but adding moving parts to the system.</p>
      <p>
          <a href="https://newsletter.francofernando.com/p/service-mesh-vs-api-gateway-part">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[The Depth-First Search Pattern: Exploring Trees and Graphs]]></title><description><![CDATA[How to systematically search through all paths by going as deep as possible before backtracking]]></description><link>https://newsletter.francofernando.com/p/the-depth-first-search-pattern-exploring</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/the-depth-first-search-pattern-exploring</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 13 Jun 2026 10:01:23 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/0c636e28-d379-483f-ba6d-91d89e8612ab_342x164.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Welcome to the 177th issue of the Polymathic Engineer newsletter.</p><p>This is the final article in our series on recursion. In the <a href="https://newsletter.francofernando.com/p/the-divide-and-conquer-breaking-problems">last article</a>, we looked at the divide-and-conquer pattern, which breaks a problem into independent subproblems, solves each one, and combines the results. This time, we examine depth-first search,  a pattern for traversing trees and graphs.</p><p>The main idea behind DFS is simple: start at a node, visit its children or neighbors recursively, and go as deep as possible before backtracking. When you reach a dead end, you return and try a different path. You keep doing this until you find what you are looking for or you run out of paths to explore.</p><p>DFS is the pattern you reach for when you need to find a path, check if something is reachable, or explore all possible paths through a structure. It works on trees, graphs, and many problems that can be represented as graphs, even if they don&#8217;t look like one at first glance.</p><p>The outline is as follows:</p><ul><li><p>The Core Pattern</p></li><li><p>Finding Paths</p></li><li><p>Handling Cycles</p></li><li><p>Finding All Paths</p></li><li><p>Matrices as Implicit Graphs</p></li><li><p>When DFS Applies</p></li></ul><div><hr></div><h4><strong><a href="https://coderabbit.link/fernando">Your AI shouldn&#8217;t grade its own homework</a></strong></h4><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!oBf2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66478278-656b-4112-a0f1-49bb03ea0435_1558x936.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!oBf2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66478278-656b-4112-a0f1-49bb03ea0435_1558x936.png 424w, https://substackcdn.com/image/fetch/$s_!oBf2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66478278-656b-4112-a0f1-49bb03ea0435_1558x936.png 848w, https://substackcdn.com/image/fetch/$s_!oBf2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66478278-656b-4112-a0f1-49bb03ea0435_1558x936.png 1272w, https://substackcdn.com/image/fetch/$s_!oBf2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66478278-656b-4112-a0f1-49bb03ea0435_1558x936.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!oBf2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66478278-656b-4112-a0f1-49bb03ea0435_1558x936.png" width="608" height="365.38461538461536" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/66478278-656b-4112-a0f1-49bb03ea0435_1558x936.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:875,&quot;width&quot;:1456,&quot;resizeWidth&quot;:608,&quot;bytes&quot;:86772,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/193795555?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66478278-656b-4112-a0f1-49bb03ea0435_1558x936.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!oBf2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66478278-656b-4112-a0f1-49bb03ea0435_1558x936.png 424w, https://substackcdn.com/image/fetch/$s_!oBf2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66478278-656b-4112-a0f1-49bb03ea0435_1558x936.png 848w, https://substackcdn.com/image/fetch/$s_!oBf2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66478278-656b-4112-a0f1-49bb03ea0435_1558x936.png 1272w, https://substackcdn.com/image/fetch/$s_!oBf2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66478278-656b-4112-a0f1-49bb03ea0435_1558x936.png 1456w" sizes="100vw" loading="lazy" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Claude Code writes beautiful code. So does Codex. But here&#8217;s the thing, they also think they write beautiful code. And when you ask an AI to review code it just wrote, you get the intellectual equivalent of a student grading their own exam. Shockingly, they always pass.</p><p><a href="https://coderabbit.link/fernando">CodeRabbit CLI</a> plugs into Claude Code and Codex as an external reviewer, different AI Agent, different architecture, 40+ static analyzers, and zero emotional attachment to the code it&#8217;s looking at. The agent writes, CodeRabbit reviews, and the agent fixes. Loop until clean.</p><p>You show up when there is actually something worth approving.</p><p>One command. Autonomous generate-review-iterate cycles. The AI still does the work. It just doesn&#8217;t get to decide if the work is good anymore.</p><p><a href="https://coderabbit.link/fernando">Free tier available. Try CodeRabbit&#8217;s CLI.</a></p><p>Thanks to the CodeRabbit team for collaborating with me on this newsletter issue.</p><div><hr></div><h2>The Core Pattern</h2><p>The simplest example of DFS is searching a binary tree to check whether it contains a particular value. We start at the root and recursively search the left and right subtrees. If we find the target value, we return true, and if we reach a null node, we return false. If either subtree contains the value, the whole tree contains it. Here is a possible way to implement this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d31d1fa1-27ba-4841-9515-c61e151d0f50&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def contains(node, target):
    if node is None:
        return False
    
    if node.value == target:
        return True
    
    return contains(node.left, target) or contains(node.right, target)</code></pre></div><p>Let&#8217;s do a quick walk-through on the following example. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!BTeK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d122a-90bb-4280-83c9-a58e7d1237c3_474x415.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BTeK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d122a-90bb-4280-83c9-a58e7d1237c3_474x415.png 424w, https://substackcdn.com/image/fetch/$s_!BTeK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d122a-90bb-4280-83c9-a58e7d1237c3_474x415.png 848w, https://substackcdn.com/image/fetch/$s_!BTeK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d122a-90bb-4280-83c9-a58e7d1237c3_474x415.png 1272w, https://substackcdn.com/image/fetch/$s_!BTeK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d122a-90bb-4280-83c9-a58e7d1237c3_474x415.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!BTeK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d122a-90bb-4280-83c9-a58e7d1237c3_474x415.png" width="366" height="320.44303797468353" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/382d122a-90bb-4280-83c9-a58e7d1237c3_474x415.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:415,&quot;width&quot;:474,&quot;resizeWidth&quot;:366,&quot;bytes&quot;:59701,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/197965772?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d122a-90bb-4280-83c9-a58e7d1237c3_474x415.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!BTeK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d122a-90bb-4280-83c9-a58e7d1237c3_474x415.png 424w, https://substackcdn.com/image/fetch/$s_!BTeK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d122a-90bb-4280-83c9-a58e7d1237c3_474x415.png 848w, https://substackcdn.com/image/fetch/$s_!BTeK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d122a-90bb-4280-83c9-a58e7d1237c3_474x415.png 1272w, https://substackcdn.com/image/fetch/$s_!BTeK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d122a-90bb-4280-83c9-a58e7d1237c3_474x415.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Suppose we want to find 4. We start at 5. As it is not the target, we proceed on the left subtree rooted at 3. Node 3 is not the target either, so we proceed by checking its left subtree, rooted at 1. Node 1 is not the target, and it has no children, so we return false.</p><p>We backtrack to 3 and search its right subtree rooted at 4. Node 4 is the target, so we return true. This true value propagates back up through 3 and then 5, and the function returns true. </p><p>This is the essence of DFS: go down one path as far as possible, and if you do not find what you are looking for, backtrack and try another path.</p><p>The time complexity is O(n), where n is the number of nodes in the tree. In the worst case, we go through each node until we identify the target or determine it is not there. The space complexity is also O(n) because of the recursion stack. This happens when the tree is completely imbalanced and resembles a linked list. So the depth is n.</p><p>One thing to keep in mind when analyzing tree problems is to be clear about what n represents. Here, n is the total number of nodes. If the tree were balanced, the depth would be O(log n), and the space complexity would improve accordingly.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.francofernando.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Polymathic Engineer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Finding Paths</h2><p>Checking whether a tree contains a value is useful, but often we need more. We would like to find the actual path from the root to the target node. This is a slight difference in how we think about the problem.</p><p>There are two approaches to do this. The first is to pass a path variable down through the recursive calls, adding nodes as we go and removing them when backtracking. The second is to build up the path as we return from the recursive calls.</p><p>I prefer the second approach. This is because at each node, we have two options as we travel down the tree: go left or go right. We do not know which way will lead us to the goal. But when we find the target and start returning, there&#8217;s only one way up. We just follow the successful path and construct the result as we go.</p><p>Here is a possible way to implement this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ea581cac-ffab-4f86-9c1a-12d29032e8d9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def path_to_node(node, target):
    if node is None:
        return None
    
    if node.value == target:
        return [node.value]
    
    left_path = path_to_node(node.left, target)
    if left_path is not None:
        return [node.value] + left_path
    
    right_path = path_to_node(node.right, target)
    if right_path is not None:
        return [node.value] + right_path
    
    return None</code></pre></div><p>The function returns None if the target is not in the subtree. Otherwise, it returns the path from the current node to the target. When the target is found, the return value is a list with just that node. As we keep returning up, each node adds itself to the front of the path.</p><p>Let&#8217;s walk through an example using the same tree as before to search for 4. We start at 5 and search the left subtree. We reach 3 and search its left subtree. We then reach 1, which has no children and is not the target, so we return None.</p><p>Back at 3, we search the right subtree. We reach 4, the target, and return [4]. Back at 3, we see that right_path is not None, so we return [3, 4]. Back at 5, we see that left_path is not None, so we return [5, 3, 4]. The path is built from the root down to the target as we return from recursive calls.</p><p>Both the time and space complexities are O(n), as for the basic containment check. We still visit each node once, and the recursion depth is at most n in an imbalanced tree.</p><h2>Handling Cycles</h2><p>The DFS pattern we have seen so far works on trees. But what about when we move to graphs? The main distinction is that a graph may contain cycles. If we are not careful, we could end up visiting the same node repeatedly and entering an infinite loop.</p><p>The solution is to mark all the nodes that we have already visited. We verify whether a node is in our visited set before traversing it. If it is, we skip it. If not, we add it to the set and keep exploring.</p><p>Here is how we can check if a path exists between two nodes in a graph:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;22439b86-e2d1-44ff-9655-e4c4799cb65b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def has_path(graph, source, destination, visited=None):
    if visited is None:
        visited = set()
    
    if source == destination:
        return True
    
    if source in visited:
        return False
    
    visited.add(source)
    
    for neighbor in graph[source]:
        if has_path(graph, neighbor, destination, visited):
            return True
    
    return False</code></pre></div><p>The structure of the code is similar to what we have seen with trees. The base case is when the current node is the target. The recursive step is to try each neighbor and see if any of them leads to the destination.</p><p>The major addition is the visited set. We check whether the current node has already been visited; if so, we return false immediately. This prevents us from going in circles. Once we visit a node, we add it to the set so we do not visit it twice.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yxp_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F042ca00a-4f79-424c-928f-e2840c1dff20_557x253.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yxp_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F042ca00a-4f79-424c-928f-e2840c1dff20_557x253.png 424w, https://substackcdn.com/image/fetch/$s_!yxp_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F042ca00a-4f79-424c-928f-e2840c1dff20_557x253.png 848w, https://substackcdn.com/image/fetch/$s_!yxp_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F042ca00a-4f79-424c-928f-e2840c1dff20_557x253.png 1272w, https://substackcdn.com/image/fetch/$s_!yxp_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F042ca00a-4f79-424c-928f-e2840c1dff20_557x253.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yxp_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F042ca00a-4f79-424c-928f-e2840c1dff20_557x253.png" width="557" height="253" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/042ca00a-4f79-424c-928f-e2840c1dff20_557x253.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:253,&quot;width&quot;:557,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:54951,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/197965772?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F042ca00a-4f79-424c-928f-e2840c1dff20_557x253.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yxp_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F042ca00a-4f79-424c-928f-e2840c1dff20_557x253.png 424w, https://substackcdn.com/image/fetch/$s_!yxp_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F042ca00a-4f79-424c-928f-e2840c1dff20_557x253.png 848w, https://substackcdn.com/image/fetch/$s_!yxp_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F042ca00a-4f79-424c-928f-e2840c1dff20_557x253.png 1272w, https://substackcdn.com/image/fetch/$s_!yxp_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F042ca00a-4f79-424c-928f-e2840c1dff20_557x253.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Let&#8217;s go through a quick example. Suppose we have a graph in which node A connects to nodes B and C, node B connects to nodes A and D, and node C connects to node A. We want to check if there is a path from A to D.</p><p>We start at A and add it to the visited set. We try neighbor B. We add B to visited and try its neighbor A. But A is already visited, so we skip it. We try B&#8217;s other neighbor D. Node D is the destination, so we return true. Without the visited set, we would have gone from A to B to A to B to A, forever.</p><p>The time complexity of the algorithm is O(V + E), where V is the number of vertices and E is the number of edges. Indeed, we visit each vertex at most once and examine each edge at most once. The space complexity is instead O(V) for the visited set and the recursion stack.</p><h2>Finding All Paths</h2><p>In the previous section, we checked whether there is a path between two nodes. But sometimes we need to find all possible paths, not just one. This requires a subtle but important change to how we handle the visited set.</p><p>When we are searching for a single path, we add a node to the visited set and leave it there. Once we know a node does not lead to the destination, there is no purpose in visiting it again. But when we are looking for all paths, different paths may share the same node. If we leave a node in visited after exploring it, other paths will not be able to use it.</p><p>The solution is backtracking. We add the node to the visited set before looking at its neighbors, then remove it when we have finished. This makes the node available for other pathways again. </p><p>Here is a possible implementation to determine all pathways between two nodes in a graph:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;56ea9d03-5834-4c3e-87a5-a81f56645cc1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def all_paths(graph, source, destination, visited=None, path=None, results=None):
    if visited is None:
        visited = set()
    if path is None:
        path = []
    if results is None:
        results = []
    
    if source in visited:
        return results
    
    visited.add(source)
    path.append(source)
    
    if source == destination:
        results.append(path[:])
    else:
        for neighbor in graph[source]:
            all_paths(graph, neighbor, destination, visited, path, results)
    
    path.pop()
    visited.remove(source)
    
    return results</code></pre></div><p>The code structure is the same as before, except for two things. First, we keep a path variable that tracks the path we are exploring. Second, after visiting all its neighbors, we delete the current node from both the visited set and the path variable.</p><p>Let&#8217;s look at an example. Suppose node A connects to nodes B and C, node B connects to node D, and node C also connects to node D. We want to find all paths from A to D.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4NrZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8786640-af8a-498e-b1ea-65439849b852_467x235.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4NrZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8786640-af8a-498e-b1ea-65439849b852_467x235.png 424w, https://substackcdn.com/image/fetch/$s_!4NrZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8786640-af8a-498e-b1ea-65439849b852_467x235.png 848w, https://substackcdn.com/image/fetch/$s_!4NrZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8786640-af8a-498e-b1ea-65439849b852_467x235.png 1272w, https://substackcdn.com/image/fetch/$s_!4NrZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8786640-af8a-498e-b1ea-65439849b852_467x235.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4NrZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8786640-af8a-498e-b1ea-65439849b852_467x235.png" width="423" height="212.85867237687367" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a8786640-af8a-498e-b1ea-65439849b852_467x235.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:235,&quot;width&quot;:467,&quot;resizeWidth&quot;:423,&quot;bytes&quot;:44377,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/197965772?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8786640-af8a-498e-b1ea-65439849b852_467x235.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4NrZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8786640-af8a-498e-b1ea-65439849b852_467x235.png 424w, https://substackcdn.com/image/fetch/$s_!4NrZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8786640-af8a-498e-b1ea-65439849b852_467x235.png 848w, https://substackcdn.com/image/fetch/$s_!4NrZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8786640-af8a-498e-b1ea-65439849b852_467x235.png 1272w, https://substackcdn.com/image/fetch/$s_!4NrZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8786640-af8a-498e-b1ea-65439849b852_467x235.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>We start at A, add it to visited and path. We try neighbor B, add it to visited and path. We try B&#8217;s neighbor D, which is the destination, so we save the path [A, B, D]. We then backtrack: remove D, then remove B from the visited set and the path.</p><p>Back at A, we try neighbor C, add it to visited and path. We try C&#8217;s neighbor D, which is the destination, so we save the path [A, C, D]. We backtrack and return both paths.</p><p>The crucial point is that D is common to both paths. If we hadn&#8217;t removed B from the visited set once we had explored it, the second path through C would still work. But in a more complicated graph, failing to remove nodes would prevent us from discovering valid paths.</p><p>In the worst case, the time complexity is O(V! * V) because there could be a factorial number of paths, and each path can contain at most V nodes. The space complexity is O(V) for the recursion stack, visited set, and current path.</p><h2>Matrices as Implicit Graphs</h2><p>Many problems involve working with matrices instead of explicit graphs. The positive news is that we can use the same DFS pattern without explicitly converting the matrix into a graph. The graph is the matrix: nodes are cells, edges are neighboring cells.</p><p>The crucial finding is that it is straightforward to compute neighbors in a matrix. If we are at cell (i, j) and can move in four directions, the neighbors are (i+1, j), (i-1, j), (i, j+1), and (i, j-1). For some problems, you can only go right or down, so the neighbors are just (i+1, j) and (i, j+1).</p><p>Let&#8217;s now look at an example. Suppose we have a matrix of integers and want to find the path with the largest product from the top-left corner to the bottom-right corner. As the following implementation shows, we can only go down or right:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1574802b-a701-401f-aa77-6cb931b9ddec&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def greatest_product_path(matrix):
    rows = len(matrix)
    cols = len(matrix[0])
    result = [float('-inf')]
    
    def dfs(i, j, product):
        if i &gt;= rows or j &gt;= cols:
            return
        
        product *= matrix[i][j]
        
        if i == rows - 1 and j == cols - 1:
            result[0] = max(result[0], product)
            return
        
        dfs(i + 1, j, product)
        dfs(i, j + 1, product)
    
    dfs(0, 0, 1)
    return result[0]</code></pre></div><p>The structure is the same as the DFS we have seen before. The base case checks if we have gone outside the matrix bounds. Every time we reach the bottom-right corner of the matrix, we compare the current product with the best product found so far. In the recursive step, we try both possible moves: down and right.</p><p>Notice that we do not need a visited set here. Since we can only move right or down, there is no way to revisit a cell. We will never form a cycle, so tracking visited nodes is unnecessary.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!59QU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5478f8ae-53ed-4266-befe-f1cee07a29a0_619x271.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!59QU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5478f8ae-53ed-4266-befe-f1cee07a29a0_619x271.png 424w, https://substackcdn.com/image/fetch/$s_!59QU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5478f8ae-53ed-4266-befe-f1cee07a29a0_619x271.png 848w, https://substackcdn.com/image/fetch/$s_!59QU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5478f8ae-53ed-4266-befe-f1cee07a29a0_619x271.png 1272w, https://substackcdn.com/image/fetch/$s_!59QU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5478f8ae-53ed-4266-befe-f1cee07a29a0_619x271.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!59QU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5478f8ae-53ed-4266-befe-f1cee07a29a0_619x271.png" width="529" height="231.5977382875606" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5478f8ae-53ed-4266-befe-f1cee07a29a0_619x271.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:271,&quot;width&quot;:619,&quot;resizeWidth&quot;:529,&quot;bytes&quot;:88725,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/197965772?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5478f8ae-53ed-4266-befe-f1cee07a29a0_619x271.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!59QU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5478f8ae-53ed-4266-befe-f1cee07a29a0_619x271.png 424w, https://substackcdn.com/image/fetch/$s_!59QU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5478f8ae-53ed-4266-befe-f1cee07a29a0_619x271.png 848w, https://substackcdn.com/image/fetch/$s_!59QU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5478f8ae-53ed-4266-befe-f1cee07a29a0_619x271.png 1272w, https://substackcdn.com/image/fetch/$s_!59QU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5478f8ae-53ed-4266-befe-f1cee07a29a0_619x271.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>This technique can be applied to many matrix problems: path finding, path counting, determining whether a destination can be reached, etc. The matrix gives the structure, and DFS provides the exploration strategy. We just need to define what a neighbor is and what the target state is.</p><p>The time complexity is O(2^(n+m)) in the worst case, where the matrix has n rows and m columns. This is because, at each cell, we have two alternatives, and the path length is at most n + m. The space complexity is O(n + m) for the recursion stack.</p><h2>When DFS Applies</h2><p>DFS is the right choice when you need to explore paths across some kind of structure, whether that structure is a tree, a graph, or something that can be represented as one. </p><p>Here are some common signs that a problem calls for DFS.</p><p>The first sign is that you need to find a path or check if one exists. If the problem asks whether you can get from point A to point B, or which path you would take to arrive there, DFS is a logical fit. You explore one path at a time, moving as deep as possible before trying alternatives.</p><p>The second sign is that you need to find all paths or explore all possibilities. If there are multiple valid routes, and you need to list them, DFS combined with backtracking does the job. You explore each path fully, record it if it is valid, then backtrack and try the next one.</p><p>The third sign is that the problem involves a tree or graph structure. This covers not only explicit trees and graphs, but also implicit ones such as matrices, game states, and decision trees. If you can define what a node is and what its neighbors are, you can apply DFS.</p><p>A few more examples of classic DFS problems:</p><ul><li><p>Determining if a graph is connected: start from any node and see if you can reach all others.</p></li><li><p>Finding connected components: run DFS from each unvisited node and mark all reachable nodes as part of the same component.</p></li><li><p>Detecting cycles in a graph: track nodes currently in the recursion stack and check if you revisit one.</p></li><li><p>Solving puzzles like mazes or Sudoku: explore one choice at a time, backtrack when you hit a dead end.</p></li><li><p>Computing properties of trees: height, diameter, path sums, and similar metrics often use DFS traversal.</p></li></ul><p>One last thing that is worth keeping in mind is the distinction between DFS and BFS (breadth-first search). DFS goes deep along a path before going wide to explore other paths, while BFS first explores all neighbors at the current level and only then moves deeper. DFS is easier to implement recursively and uses less memory for deep, narrow structures. BFS is better when you need the shortest path in an unweighted graph.</p><h2>Conclusion</h2><p>DFS is one of the most flexible patterns for recursion. The basic principle is straightforward. Take one path as far as you can, and if it doesn&#8217;t get to where you want, backtrack and try another.</p><p>When it comes to trees, the pattern is simple. You traverse nodes recursively, returning results as you come back up. When dealing with graphs, you have to keep track of visited nodes to avoid cycles. In matrix problems, cells are nodes and neighboring cells are neighbors. The logic is always the same.</p><p>This brings us to the end of our series on recursion. We have covered six patterns:</p><ul><li><p><strong><a href="https://newsletter.francofernando.com/p/recursion-in-practice-iteration-and">Iteration</a></strong>: process items one at a time and collect results as you go.</p></li><li><p><strong><a href="https://newsletter.francofernando.com/p/recursion-in-practice-iteration-and">Subproblems</a></strong>: solve a smaller version of the problem and build on it.</p></li><li><p><strong><a href="https://newsletter.francofernando.com/p/finding-all-combinations-with-the">Selection</a></strong>: choose whether to include or exclude each item when creating combinations.</p></li><li><p><strong><a href="https://newsletter.francofernando.com/p/the-ordering-pattern-generating-all">Ordering</a></strong>: determine which item comes next to produce permutations.</p></li><li><p><strong><a href="https://newsletter.francofernando.com/p/the-divide-and-conquer-breaking-problems">Divide and conquer</a></strong>: split the input into independent parts, solve each, and combine.</p></li><li><p><strong>Depth-first search</strong>: explore paths by going deep before going wide.</p></li></ul><p>These patterns are not mutually exclusive. Many problems combine elements of several patterns. A backtracking problem might use selection with DFS. A tree problem might use divide and conquer with subproblems. The more comfortable you become with each pattern, the easier it is to recognize which ones apply and how to combine them.</p><p>Recursion can feel tricky at first, but it becomes natural with practice. The key is to focus on two things: the base case and the recursive step. Get those right, and the rest takes care of itself.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.francofernando.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Polymathic Engineer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[What a Service Mesh Gives You (Part II)]]></title><description><![CDATA[The features that make a service mesh worth the trouble.]]></description><link>https://newsletter.francofernando.com/p/what-a-service-mesh-gives-you-part</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/what-a-service-mesh-gives-you-part</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 06 Jun 2026 10:27:13 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!OITG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b53b019-6738-41f7-b7aa-0d1581708d75_1214x447.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Welcome to the 176th issue of the Polymathic Engineer newsletter.</p><p>This week, we continue our series of articles on service mesh. In the previous article, we discussed what a service mesh is and how it works. We&#8217;ve seen that a service mesh is a separate layer of infrastructure that handles all the traffic between services. The key idea is the sidecar pattern, where a small proxy runs next to each service instance and quietly takes over the networking job.</p><p>Now that we understand what a service mesh is, the natural next question is what you really get out of it. The short answer is that it gives you 4 things: traffic management, reliability, security, and observability. All microservice systems have to deal with these issues, and a service mesh solves them all in one place.</p><p>Another benefit that runs through all of these is polyglot support. As we have seen in the previous article, the library approach breaks down when teams use more than one programming language. A service mesh avoids this issue because it does everything in the proxy layer. Regardless of whether a service is written in Go, Python, Java, or C#, it gets the same retries, the same mTLS, the same metrics, and the same routing rules. There is no library for each language to build and maintain.</p><p>However, not all is for free. A service mesh has a cost in terms of latency, complexity, and resources. First, we will talk about the beneficial things, then about the trade-offs, and finally, we&#8217;ll take a quick look at the most typical service mesh implementations.</p><p>The outline is as follows:</p><ul><li><p>Traffic management and routing</p></li><li><p>Reliability</p></li><li><p>Security</p></li><li><p>Observability</p></li><li><p>Trade-offs: when a mesh isn&#8217;t worth it</p></li><li><p>A quick tour of popular meshes</p></li></ul><div><hr></div><p>Project-based learning is the best way to develop technical skills. <a href="https://app.codecrafters.io/join?via=FrancoFernando">CodeCrafters </a>is an excellent platform for tackling exciting projects, such as building your own Redis, Kafka, a <a href="https://app.codecrafters.io/join/dns-server?via=francofernando">DNS server</a>, SQLite, an HTTP server, or Git from scratch using your favorite programming language. Now you can also try to build your own Claude Code (for free, still in beta).</p><p><a href="https://app.codecrafters.io/join?via=FrancoFernando">Sign up, and become a better software engineer</a>.</p><div><hr></div><h2>Traffic management and routing</h2><p>The first thing a service mesh gives you is control over how traffic flows between your services.</p><p>When a service wants to call another service, it needs to know its address. In the past, when data centers were on-premises, these addresses were usually hardcoded because machines rarely changed. This isn't true anymore in the cloud. Instances come and go, IP addresses change, and services often move to different nodes. Your code will break whenever the network layout shifts if you hardcode addresses.</p><p>A service mesh takes care of this for you through service discovery. The sidecar keeps track of where each service lives in real time. The proxy finds out the service that the application is calling by name and directs the request there. The application doesn&#8217;t need to know any IP addresses, and it doesn&#8217;t need to be redeployed when something moves.</p><p>On top of this, the mesh can normalize service names across environments. The code might, for example, call a service called sessions-service. Then, when the code is put into production, it talks to a production instance. When it is put into staging, it talks to a staging instance. The mesh knows which one to use based on where it is running, so the application doesn&#8217;t have to carry environment-specific configuration.</p><p>Another key feature is load balancing. When there are multiple instances of a service, the mesh spreads the traffic across them. Because the sidecar understands application-level protocols like HTTP/2 and gRPC, it can do request-level load balancing, not just connection-level. This is important since one gRPC connection can handle numerous requests. If there is no request-level balancing, one connection would pin all the work to a single instance.</p><p>Beyond simple balancing, a service mesh supports more advanced traffic control. We can split traffic between two versions of a service: for example, send 95% of the calls to v1 and 5% to v2 to roll out a new release gradually. This is how canary deployments work at the infrastructure layer, without having to touch the application. We can also mirror traffic, sending a copy of the requests to a test instance to see how it handles production load, without affecting the real responses.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OITG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b53b019-6738-41f7-b7aa-0d1581708d75_1214x447.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OITG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b53b019-6738-41f7-b7aa-0d1581708d75_1214x447.png 424w, https://substackcdn.com/image/fetch/$s_!OITG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b53b019-6738-41f7-b7aa-0d1581708d75_1214x447.png 848w, https://substackcdn.com/image/fetch/$s_!OITG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b53b019-6738-41f7-b7aa-0d1581708d75_1214x447.png 1272w, https://substackcdn.com/image/fetch/$s_!OITG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b53b019-6738-41f7-b7aa-0d1581708d75_1214x447.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OITG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b53b019-6738-41f7-b7aa-0d1581708d75_1214x447.png" width="674" height="248.16968698517297" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5b53b019-6738-41f7-b7aa-0d1581708d75_1214x447.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:447,&quot;width&quot;:1214,&quot;resizeWidth&quot;:674,&quot;bytes&quot;:75884,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/195329029?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b53b019-6738-41f7-b7aa-0d1581708d75_1214x447.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!OITG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b53b019-6738-41f7-b7aa-0d1581708d75_1214x447.png 424w, https://substackcdn.com/image/fetch/$s_!OITG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b53b019-6738-41f7-b7aa-0d1581708d75_1214x447.png 848w, https://substackcdn.com/image/fetch/$s_!OITG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b53b019-6738-41f7-b7aa-0d1581708d75_1214x447.png 1272w, https://substackcdn.com/image/fetch/$s_!OITG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b53b019-6738-41f7-b7aa-0d1581708d75_1214x447.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Traffic shaping and traffic policing are other useful tools. Shaping is when you slow down some traffic to make it fit a certain pattern. For example, you might give paying customers priority over free-tier users. Policing is making sure a contract is followed. For example, you could block a service that makes too many requests to a downstream dependency. Both are ways to keep the system stable when some parts behave badly.</p><p>You can set all of this without changing the application code. The mesh configuration is stored in files that the platform or infrastructure teams can update, and the sidecars pick up the changes.</p><h2>Reliability</h2><p>Networks fail. Services can become slow or unresponsive. Instances can crash or get too loaded during traffic spikes. In microservice systems, any call between services is a potential point of failure. If you don&#8217;t manage these failures in a consistent way, the issues spread through the system very quickly.</p><p>A service mesh can apply the well-known reliability patterns to every call, without the applications having to know about them.</p><p>The first one is retries. If a call fails because of a temporary error, the sidecar retries it automatically, usually with exponential backoff. This means that the proxy waits a bit longer between each attempt to avoid making things worse when a service is already having trouble. The mesh can also distinguish between errors that are worth retrying, like a network timeout, and errors that are not, like a 404 response.</p><p>Timeouts work in the same way. The sidecar gives up instead of waiting forever when a call takes too long. This is important because slow calls can add up quickly.If there is no timeout, a slow downstream service can use up all the system&#8217;s resources while every call upstream waits for a response that never comes. </p>
      <p>
          <a href="https://newsletter.francofernando.com/p/what-a-service-mesh-gives-you-part">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Service Mesh: Why and How It Works (Part I)]]></title><description><![CDATA[How a dedicated infrastructure layer can take over the job of managing service-to-service communication.]]></description><link>https://newsletter.francofernando.com/p/service-mesh-why-and-how-it-works</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/service-mesh-why-and-how-it-works</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 30 May 2026 09:34:40 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!KlZL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a2b7ab7-d16b-48d6-bf48-86c44126fb61_1176x307.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Welcome to the 175th issue of the Polymathic Engineer newsletter.</p><p>Building software has changed a lot in the last decade. We moved from big monolithic applications to systems made up of many smaller services that talk to each other over the network. This change helped teams in several ways, such as the possibility to pick the right language for each job, faster development, and independent deployments.</p><p>But there is something to consider. A call to a function inside a monolith is now a call between two services over the network. And network calls are a whole different story. They can be slow, break or become lost. We have to think about things like balancing the load across service instances, retrying failed requests, encrypting traffic between services, and deciding which service can call which. </p><p>These are the kinds of problems a service mesh is built to solve.</p><p>At a high level, a service mesh is a dedicated layer that sits between your services and handles all the network traffic between them. The mesh handles encryption, timeouts, retries, and service discovery, so the services don&#8217;t need to take care of them in their own code. The application code makes a standard HTTP or gRPC call, and the mesh does the heavy lifting behind the scenes.</p><p>In this issue, we break down the fundamentals of service mesh: why the pattern exists, what it actually is, and how its architecture works. This is the first of three articles in a series. In the next two, we will talk about what a service mesh gives you in terms of features and how it stacks up against an API gateway.</p><p>The outline is as follows:</p><ul><li><p>Why we need a service mesh</p></li><li><p>What a service mesh is</p></li><li><p>The sidecar pattern</p></li><li><p>Data plane and control plane</p></li><li><p>Where a service mesh is deployed</p></li></ul><div><hr></div><p>Project-based learning is the best way to develop technical skills. <a href="https://app.codecrafters.io/join?via=FrancoFernando">CodeCrafters </a>is an excellent platform for tackling exciting projects, such as building your own Redis, Kafka, a <a href="https://app.codecrafters.io/join/dns-server?via=francofernando">DNS server</a>, SQLite, an HTTP server, or Git from scratch using your favorite programming language. Now you can also try to build your own Claude Code (for free, still in beta).</p><p><a href="https://app.codecrafters.io/join?via=FrancoFernando">Sign up, and become a better software engineer</a>.</p><div><hr></div><h2>Why we need a service mesh</h2><p>As we discussed, switching to microservices made it harder for services to talk to each other. These problems are not new. Anybody who has worked on distributed systems knows that network calls are slow and unreliable, and getting services to trust each other is hard. The size is what changed.  With dozens of services talking to each other, every engineering team has to deal with these difficulties daily.</p><p>The first attempt to solve this was the library approach. Teams would create a shared library that managed things like retries, circuit breaking, load balancing, and service discovery, and every service would use it as a dependency. The Netflix stack from the early 2010s is a well-known example. It had Eureka for service discovery, Hystrix for circuit breakers, and Ribbon for client-side load balance. For a while, it worked well, and it became the standard way to build reliable microservice architectures. But the approach started to show its flaws as systems got bigger and teams used more than one programming language.</p><p>The biggest issue was that every programming language needed its own version of the library. If the team used C#, Python, and Go, they needed three implementations that behaved the same way. This is tougher than it sounds because even slight changes in how timeouts or retries are handled might make the system behave differently.</p><p>Another problem was version skew. Different services would use different versions of the library at any given time. To roll out a bugfix in the retry logic, every team had to redeploy their services. This slowed down changes that should have been easy to make.</p><p>Last but not least, the library model put a lot of work on the application teams to keep things running. Every developer had to know how to set up circuit breakers, discovery clients, and retries, although these have nothing to do with the business problem they were trying to solve. The code was harder to read, test, and change since application logic and networking logic were mixed together.</p><p>These limitations made engineering teams look for another approach. If networking concerns were the same across services, and if the library version of it was painful to maintain across languages, maybe the best thing to do was to move them out of the application altogether. </p><p>This is the idea behind a service mesh.</p><h2>What a service mesh is</h2><p>A service mesh is a separate layer of infrastructure that handles all the traffic between the services in a distributed system. It takes care of routing, load balancing, timeouts, retries, encryption, and observability, without the services having to be aware of it.</p><p>The last sentence is the main point. In the library approach we discussed before, each service had to consume a dependency and call specific APIs to get circuit breaking or retries. With a service mesh, the service just makes a standard call to another service. The mesh sits in the network path and quietly adds the extra behavior before the call reaches its destination.</p><p>This kind of traffic, where one internal service talks to another, is usually called east-west traffic. Traffic that comes from outside the system, such as a mobile app or a web browser calling a backend, is called north-south traffic. These 2 kinds of traffic have different needs and patterns. A user may only send a request to the backend once in a while, but with a microservice design, one request can lead to dozens of calls between services. This is why service mesh tools focus on east-west traffic and look different from the tools that manage traffic at the edge of the system. We will come back to this comparison in the third part of the series.</p><p>So how does a service mesh sit in the middle of every service call without the services having to know? The answer is the sidecar pattern.</p><h2>The sidecar pattern</h2><p>The sidecar pattern is the trick that makes a service mesh work. The key idea is to run a small proxy process next to each service instance, on the same machine or inside the same pod if we are on Kubernetes. The proxy is always deployed with the service as a single unit. This is why it is termed a &#8220;sidecar,&#8221; like the sidecar of a motorbike.</p>
      <p>
          <a href="https://newsletter.francofernando.com/p/service-mesh-why-and-how-it-works">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[The Divide and Conquer: Breaking Problems into Smaller Pieces]]></title><description><![CDATA[How to split a problem into independent subproblems, solve each one, and combine the results]]></description><link>https://newsletter.francofernando.com/p/the-divide-and-conquer-breaking-problems</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/the-divide-and-conquer-breaking-problems</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 23 May 2026 09:30:46 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Acz9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72e87bd8-a7c8-4675-8f73-6c202e6915f3_3091x883.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Welcome to the 174th issue of the Polymathic Engineer newsletter.</p><p>In the last article in our series on recursion, we looked at the ordering pattern, which lets us generate all permutations of a set of items. This time, we talk about a different method: divide and conquer. </p><p>Instead of working on items one at a time, the main idea is to partition the input into multiple independent chunks, solve each separately, then combine the results.</p><p>If you have worked with algorithms, you have surely already seen divide and conquer in action. Binary search divides an array in half and searches only on the proper side. Merge sort splits an array in half, sorts both halves, and merges them back together. Tree traversals split a tree into left and right subtrees and visit each one individually.</p><p>The important thing is that we are not just taking something out and solving a smaller problem. We are breaking the input into several non-trivial subproblems and merging their solutions. That is what makes divide and conquer so powerful for some types of problems.</p><p>The outline is as follows:</p><ul><li><p>The Core Pattern</p></li><li><p>Fixed Splitting: Binary Search</p></li><li><p>Trying All Splits: Unique Binary Search Trees</p></li><li><p>Other Divide and Conquer Examples</p></li><li><p>When Divide and Conquer Applies</p></li></ul><div><hr></div><p>Project-based learning is the best way to develop technical skills. <a href="https://app.codecrafters.io/join?via=FrancoFernando">CodeCrafters </a>is an excellent platform for tackling exciting projects, such as building your own Redis, Kafka, a <a href="https://app.codecrafters.io/join/dns-server?via=francofernando">DNS server</a>, SQLite, an HTTP server, or Git from scratch using your favorite programming language. Now you can also try to build your own Claude Code (for free, still in beta).</p><p><a href="https://app.codecrafters.io/join?via=FrancoFernando">Sign up, and become a better software engineer</a>.</p><div><hr></div><h2>The Core Pattern</h2><p>The divide and conquer pattern follows three steps. First, we divide the input into two or more smaller pieces. Then, we conquer each piece by solving it recursively. Finally, we combine the results from each piece to get the solution for the original input.</p><p>This is different from what we did with the iteration and subproblems patterns. With iteration, we processed one item at a time and collected the result. With subproblems, we removed one element and solved a smaller version of the same problem. In both cases, we were reducing the input by one item.</p><p>Using divide and conquer, we partition the input into multiple nontrivial chunks. For example, if we have an array of 8 elements, we might split it into two arrays of 4. Each of those gets split into 2 arrays of 2. We keep splitting until we reach a base case, solve each base case, and then merge the results back up.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Acz9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72e87bd8-a7c8-4675-8f73-6c202e6915f3_3091x883.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Acz9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72e87bd8-a7c8-4675-8f73-6c202e6915f3_3091x883.png 424w, https://substackcdn.com/image/fetch/$s_!Acz9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72e87bd8-a7c8-4675-8f73-6c202e6915f3_3091x883.png 848w, https://substackcdn.com/image/fetch/$s_!Acz9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72e87bd8-a7c8-4675-8f73-6c202e6915f3_3091x883.png 1272w, https://substackcdn.com/image/fetch/$s_!Acz9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72e87bd8-a7c8-4675-8f73-6c202e6915f3_3091x883.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Acz9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72e87bd8-a7c8-4675-8f73-6c202e6915f3_3091x883.png" width="714" height="204" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/72e87bd8-a7c8-4675-8f73-6c202e6915f3_3091x883.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:416,&quot;width&quot;:1456,&quot;resizeWidth&quot;:714,&quot;bytes&quot;:145398,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/196046548?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72e87bd8-a7c8-4675-8f73-6c202e6915f3_3091x883.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Acz9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72e87bd8-a7c8-4675-8f73-6c202e6915f3_3091x883.png 424w, https://substackcdn.com/image/fetch/$s_!Acz9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72e87bd8-a7c8-4675-8f73-6c202e6915f3_3091x883.png 848w, https://substackcdn.com/image/fetch/$s_!Acz9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72e87bd8-a7c8-4675-8f73-6c202e6915f3_3091x883.png 1272w, https://substackcdn.com/image/fetch/$s_!Acz9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72e87bd8-a7c8-4675-8f73-6c202e6915f3_3091x883.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The main criterion is that the subproblems must be independent. The solution of one part should not depend on the solution of another part. This is what allows us to solve them separately and then combine the results.</p><p>There are two main variations of this pattern, depending on whether we know where to split.</p><p>In the first variation, we know exactly where to divide the input. Binary search always splits in the middle. Merge sort always divides in the middle. The split point is fixed, and the only decision is which half to look at in the case of binary search, or how to merge the halves in the case of merge sort.</p><p>In the second variation, we do not know where to split. We have to try every possible split point, solve both sides for each split, and see which one gives us the best result. This comes up in grouping problems, where we need to find all ways to partition an input or the optimal way to group elements together.</p><p>We will look at examples of both variations in the following sections.</p><h2>Fixed Splitting: Binary Search</h2><p>Binary search is the simplest example of a divide and conquer algorithm with a fixed split point. We always divide the array in the middle and decide which half to search based on a comparison.</p><p>The problem is straightforward: given a sorted array and a target value, check whether the target is present in the array. The brute-force approach would have to check every element, which takes O(n) time. Binary search does it in O(log n) by halving the search space at each step.</p><p>This is how the algorithm works. We examine the middle element of the array. If it is equal to the target, we are done. If the target is smaller, we search the left half. If the target is larger, we search the right half. We keep doing this until we find the target or run out of elements to check.</p><p>Here is a possible way to implement this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;863726ea-f2e5-4408-80d1-b40cc5e52772&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def binary_search(arr, target, low, high):
    if low &gt; high:
        return False
    
    mid = (low + high) // 2
    
    if arr[mid] == target:
        return True
    elif arr[mid] &gt; target:
        return binary_search(arr, target, low, mid - 1)
    else:
        return binary_search(arr, target, mid + 1, high)</code></pre></div><p>Let&#8217;s walk through an example to grasp how this works. Say we have the array [1, 2, 3, 5, 7, 9, 11, 13] and we are looking for 3. We start with low = 0 and high = 7. The middle index is 3, and the element at that index is 5. Since 3 is smaller than 5, we continue our search in the left half: low = 0, high = 2.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yj9G!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ea0b534-c31a-46fb-b8b7-eeff3e728103_1039x629.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yj9G!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ea0b534-c31a-46fb-b8b7-eeff3e728103_1039x629.png 424w, https://substackcdn.com/image/fetch/$s_!yj9G!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ea0b534-c31a-46fb-b8b7-eeff3e728103_1039x629.png 848w, https://substackcdn.com/image/fetch/$s_!yj9G!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ea0b534-c31a-46fb-b8b7-eeff3e728103_1039x629.png 1272w, https://substackcdn.com/image/fetch/$s_!yj9G!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ea0b534-c31a-46fb-b8b7-eeff3e728103_1039x629.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yj9G!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ea0b534-c31a-46fb-b8b7-eeff3e728103_1039x629.png" width="496" height="300.2733397497594" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4ea0b534-c31a-46fb-b8b7-eeff3e728103_1039x629.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:629,&quot;width&quot;:1039,&quot;resizeWidth&quot;:496,&quot;bytes&quot;:68482,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/196046548?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ea0b534-c31a-46fb-b8b7-eeff3e728103_1039x629.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yj9G!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ea0b534-c31a-46fb-b8b7-eeff3e728103_1039x629.png 424w, https://substackcdn.com/image/fetch/$s_!yj9G!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ea0b534-c31a-46fb-b8b7-eeff3e728103_1039x629.png 848w, https://substackcdn.com/image/fetch/$s_!yj9G!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ea0b534-c31a-46fb-b8b7-eeff3e728103_1039x629.png 1272w, https://substackcdn.com/image/fetch/$s_!yj9G!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ea0b534-c31a-46fb-b8b7-eeff3e728103_1039x629.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now the middle index becomes 1, and the element there is 2. Since 3 is larger than 2, we search the right half: low = 2, high = 2. The middle index is 2, and the element at that index is the target, so we return true.</p><p>The time complexity is O(log n) because we cut the search space in half at each step. If we have 8 elements, we need at most 3 comparisons. If we have 1024 elements, at most 10 comparisons are needed. The space complexity is also O(log n) due to the recursion stack.</p><p>The crucial thing to observe is that we always know exactly where to split. No need to attempt different split points. The midpoint is always the appropriate choice, and the comparison tells us which half to search. This is why binary search is so efficient.</p><h2>Trying All Splits: Unique Binary Search Trees</h2><p>Now, let&#8217;s look at a problem where we do not know where to divide. Instead of always splitting in the middle, we try every possible split point and combine the results.</p>
      <p>
          <a href="https://newsletter.francofernando.com/p/the-divide-and-conquer-breaking-problems">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Bloom Filters Explained ]]></title><description><![CDATA[Fast lookups with a fraction of the memory, when 'probably yes' is good enough]]></description><link>https://newsletter.francofernando.com/p/bloom-filters-explained</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/bloom-filters-explained</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 16 May 2026 07:29:59 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!7X0_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0839281-aca3-451a-ac0a-d0bf35d4f8aa_750x414.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Welcome to the 173rd issue of the Polymathic Engineer newsletter.</p><p><a href="https://newsletter.francofernando.com/p/the-dictionary-problem-fast-lookups">In a previous article</a>, we talked about the dictionary problem and discussed different types of data structures that solve it, from unsorted arrays to hash tables and binary search trees. We have seen that hash tables are the best choice when all you need is to check if something is in a collection, since you can look up, add, and remove items in O(1) time.</p><p>But we have also seen that hash tables come with a cost: memory. Storing 100K words in a hash table means keeping all those words in memory, plus the extra space that the hash table needs. What if you are working in a context where memory is limited, like a router processing millions of packets per second, or a distributed database that needs to quickly check if an entry exists before writing to disk?</p><p>This is where Bloom filters come in. A Bloom filter is a data structure that can tell you if an item is in a set using only a small amount of memory with respect to a hash table. The catch is that it is not always right. A Bloom filter can tell you with certainty that something is not in the set. However, when it indicates something is there, it could be inaccurate. These inaccurate answers are known as false positives, and as we will see,  you get to control how often they happen.</p><p>In this article, we will look in detail at how Bloom filters work, why they can give false positives but never false negatives, how to tune them to fit your needs, and where they are used in practice. This is the outline:</p><ul><li><p>How Bloom filters differ from hash tables</p></li><li><p>How Bloom filters work</p></li><li><p>Why are there no false negatives</p></li><li><p>Why are there false positives</p></li><li><p>Tuning the filter</p></li><li><p>Real-world use cases</p></li></ul><div><hr></div><p>Project-based learning is the best way to develop technical skills. <a href="https://app.codecrafters.io/join?via=FrancoFernando">CodeCrafters </a>is an excellent platform for tackling exciting projects, such as building your own Redis, Kafka, a <a href="https://app.codecrafters.io/join/dns-server?via=francofernando">DNS server</a>, SQLite, an HTTP server, or Git from scratch using your favorite programming language. Now you can also try to build your own Claude Code (for free, still in beta).</p><p><a href="https://app.codecrafters.io/join?via=FrancoFernando">Sign up, and become a better software engineer</a>.</p><div><hr></div><h2>How Bloom filters differ from hash tables</h2><p>Before we go into the details, it is worth knowing what makes Bloom filters different from hash tables, since both use hashing under the hood. There are three main things to keep in mind.</p><p>First, Bloom filters don't store the actual data. A hash table stores the keys (and maybe their values) in memory. A Bloom filter doesn't do that. The sole question it answers is whether or not an item is in the set.</p><p>Second, Bloom filters use considerably less memory. This is the main reason why they exist. A hash table for 100,000 strings needs to store all those strings plus the internal structure of the table. A Bloom filter for the same 100K strings only needs a bit array, regardless of how long the strings are.</p><p>Third, Bloom filters can give wrong answers. When a Bloom filter says an item is not in the set, it is always right. But it could be inaccurate when it indicates an item is in the set. This is a false positive. There is a trade-off between the memory a Bloom filter employs and how often these false positives happen. The less memory, the more false positives.</p><p>There is also one more thing to be aware of: the basic version of Bloom filters doesn&#8217;t support deletion. Once you add an item, you can&#8217;t remove it. Some advanced variants have been developed to handle removal, but the classic version does not.</p><p>If you can live with these constraints, you get a data structure that is as fast as a hash table for lookups and insertions, but uses only a fraction of the memory.</p><h2>How Bloom filters work</h2><p>A Bloom filter is made of two things: a bit array with m elements, all initially set to 0, and a set of k hash functions. Each hash function takes a key and returns an index in the range between 0 and m-1.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!12yE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff6ea9a-c162-4f73-ac8d-f43132cd9ef4_750x162.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!12yE!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff6ea9a-c162-4f73-ac8d-f43132cd9ef4_750x162.png 424w, https://substackcdn.com/image/fetch/$s_!12yE!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff6ea9a-c162-4f73-ac8d-f43132cd9ef4_750x162.png 848w, https://substackcdn.com/image/fetch/$s_!12yE!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff6ea9a-c162-4f73-ac8d-f43132cd9ef4_750x162.png 1272w, https://substackcdn.com/image/fetch/$s_!12yE!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff6ea9a-c162-4f73-ac8d-f43132cd9ef4_750x162.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!12yE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff6ea9a-c162-4f73-ac8d-f43132cd9ef4_750x162.png" width="750" height="162" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4ff6ea9a-c162-4f73-ac8d-f43132cd9ef4_750x162.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:162,&quot;width&quot;:750,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:29229,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/194079630?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff6ea9a-c162-4f73-ac8d-f43132cd9ef4_750x162.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!12yE!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff6ea9a-c162-4f73-ac8d-f43132cd9ef4_750x162.png 424w, https://substackcdn.com/image/fetch/$s_!12yE!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff6ea9a-c162-4f73-ac8d-f43132cd9ef4_750x162.png 848w, https://substackcdn.com/image/fetch/$s_!12yE!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff6ea9a-c162-4f73-ac8d-f43132cd9ef4_750x162.png 1272w, https://substackcdn.com/image/fetch/$s_!12yE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff6ea9a-c162-4f73-ac8d-f43132cd9ef4_750x162.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The most important thing to remember is that there is no direct connection between the bits in the array and the keys you add to the filter. You specify the value of k when you create the filter. Each key is stored using k bits, one for each hash function. This means that every key you add takes up the same amount of memory, k bits, no matter how lengthy it is. A 5-character word and a 500-character URL both use k bits.</p><h4><strong>Inserting an element</strong></h4><p>When you add a key to the filter, you give it as an input to all the k hash functions. Each function gives you an index in the bit array, and you set the bits at those k positions to 1. For example, let&#8217;s suppose we have a Bloom filter with 15 bits and 3 hash functions, and we want to add the word &#8220;binary.&#8221; We compute:</p><ul><li><p>h0(&#8221;binary&#8221;) = 2</p></li><li><p>h1(&#8221;binary&#8221;) = 6</p></li><li><p>h2(&#8221;binary&#8221;) = 9</p></li></ul>
      <p>
          <a href="https://newsletter.francofernando.com/p/bloom-filters-explained">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[The Ordering Pattern: Generating All Permutations]]></title><description><![CDATA[How to find every possible arrangement of a set of items]]></description><link>https://newsletter.francofernando.com/p/the-ordering-pattern-generating-all</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/the-ordering-pattern-generating-all</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 09 May 2026 08:02:08 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!J-kl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32f854f5-339c-40ea-8ab6-97762544414f_1912x863.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Welcome to the 172nd issue of the Polymathic Engineer newsletter. </p><p>In the last article in our series on recursion, we looked at the selection pattern, which lets us find all possible subsets of a set. The most important question was whether or not to include each item.</p><p>In this article, we shift our focus from which items to include to how to arrange them. This is the ordering pattern, and its purpose is to generate all permutations of a set of items.</p><p>The difference between combinations and permutations is easy. With combinations, [1, 2] and [2, 1] are the same because the order of the elements does not matter. With permutations, the order matters. A set of three items, such as [1, 2, 3], has 8 possible subsets but 6 possible permutations: [1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1].</p><p>Ordering problems come up a lot in practice. Every time you need to find all possible ways to arrange, schedule, or sequence anything, you need to generate permutations. And, like selection, the ordering pattern employs backtracking to consider all options.</p><p>The outline is as follows:</p><ul><li><p>The Core Ordering Pattern</p></li><li><p>The Swap-Based Approach</p></li><li><p>Permutations of Specific Length</p></li><li><p>Handling Duplicates in the Input</p></li><li><p>Application: Binary Search Tree Orderings</p></li><li><p>When Ordering Applies</p></li></ul><div><hr></div><p>Project-based learning is the best way to develop technical skills. <a href="https://app.codecrafters.io/join?via=FrancoFernando">CodeCrafters </a>is an excellent platform for tackling exciting projects, such as building your own Redis, Kafka, a <a href="https://app.codecrafters.io/join/dns-server?via=francofernando">DNS server</a>, SQLite, HTTP server or Git from scratch using your favourite programming language. Noe you can also try to build your own Claude Code (for free, still in beta).</p><p><a href="https://app.codecrafters.io/join?via=FrancoFernando">Sign up, and become a better software engineer</a>.</p><div><hr></div><h2>The Core Ordering Pattern</h2><p>While selection goes through each item and asks whether to include it, ordering takes a different approach. Instead of making include/exclude decisions, it asks: which item comes next?</p><p>The underlying idea is to keep a set of available items. At each step, we pick one item from the set, add it to our current permutation, remove it from the set, and recursively find all permutations of the remaining items. When there are no remaining items, we have built one complete permutation.</p><p>Let&#8217;s start with a simple example, with three items: [1, 2, 3]. In the beginning, all items are available. We could choose 1 first, which leaves us with [2, 3]. From there, we could pick 2, which leaves us with only [3]. Then we can only choose 3, and now we have our first permutation: [1, 2, 3].</p><p>At this point, we backtrack. We put 3 back, put 2 back, and try picking 3 instead. This gives us [1, 3, 2]. We continue this process, trying every possible first item, then every possible second item, and so on.</p><p>The following code snippet shows how to implement this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;16bd9bd0-363b-4f34-8a2a-4472b8fa3961&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def permutations(items):
    result = []
    available = set(items)
    
    def helper(path):
        if not available:
            result.append(path[:])
            return
        
        for item in list(available):
            available.remove(item)
            path.append(item)
            helper(path)
            path.pop()
            available.add(item)
    
    helper([])
    return result</code></pre></div><p>The structure is quite similar to what we have seen with selection. We have a helper function that builds the permutation step by step. The base case is when there are no items left to choose from, meaning we have used all of them and produced a complete permutation.</p><p>In the recursive step, we go through each available item. We take each one out of the set, add it to the path, and call the function again. After the call is over, we go back by taking the item off the path and putting it back in the set.</p><p>One thing to note is that we iterate over a copy of the list of available items instead of iterating over them directly. This is because we are modifying the set inside the loop, and most languages do not allow you to change a collection while iterating over it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!J-kl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32f854f5-339c-40ea-8ab6-97762544414f_1912x863.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!J-kl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32f854f5-339c-40ea-8ab6-97762544414f_1912x863.png 424w, https://substackcdn.com/image/fetch/$s_!J-kl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32f854f5-339c-40ea-8ab6-97762544414f_1912x863.png 848w, https://substackcdn.com/image/fetch/$s_!J-kl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32f854f5-339c-40ea-8ab6-97762544414f_1912x863.png 1272w, https://substackcdn.com/image/fetch/$s_!J-kl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32f854f5-339c-40ea-8ab6-97762544414f_1912x863.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!J-kl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32f854f5-339c-40ea-8ab6-97762544414f_1912x863.png" width="1456" height="657" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/32f854f5-339c-40ea-8ab6-97762544414f_1912x863.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:657,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:190871,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/82350676?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32f854f5-339c-40ea-8ab6-97762544414f_1912x863.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!J-kl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32f854f5-339c-40ea-8ab6-97762544414f_1912x863.png 424w, https://substackcdn.com/image/fetch/$s_!J-kl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32f854f5-339c-40ea-8ab6-97762544414f_1912x863.png 848w, https://substackcdn.com/image/fetch/$s_!J-kl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32f854f5-339c-40ea-8ab6-97762544414f_1912x863.png 1272w, https://substackcdn.com/image/fetch/$s_!J-kl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32f854f5-339c-40ea-8ab6-97762544414f_1912x863.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The time complexity is O(n! &#215; n). The branching factor starts at n and decreases by one at each level, which gives us n &#215; (n-1) &#215; (n-2) &#215; ... &#215; 1 = n! paths. Each time we complete a permutation, we then need O(n) time to copy it into the result. The space complexity is O(n) for the recursion stack and the path we are building.</p><h2>The Swap-Based Approach</h2><p>The set-based approach we looked at is straightforward, but there is a more efficient way that you can also use in interviews. This method does not store a separate list of available items. Instead, it uses the input array itself to track what has been used and what remains available.</p><p>The underlying idea is to divide the array into two parts using an index. All the items before the index already belong to the permutation we are generating. The items from the index onward are the pool of items we can still choose from. At each step, we swap an item from the available portion into the current position and then move to the next index. Let&#8217;s go back to our simple example, [1, 2, 3], to grasp how the process works. </p><p>Initially, the index is equal to 0 because the entire array is available. We swap the first item with itself, which doesn&#8217;t change anything, then we move to index 1. We can now choose either 2 or 3. We choose 2 and swap the second item with itself. Then we go to index 2, pick 3, and get our first permutation: [1, 2, 3]. At this point, we backtrack.</p><p>We go back to index 1 and swap positions 1 and 2. This places 3 in position 1 and 2 in position 2, yielding [1, 3, 2]. We keep doing this until we have executed every potential swap at every slot. </p><p>The following code snippet shows how to implement this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;49352de8-9b47-4ea6-a592-85ca6c958422&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def permutations(items):
    result = []
    
    def helper(index):
        if index == len(items):
            result.append(items[:])
            return
        
        for j in range(index, len(items)):
            items[index], items[j] = items[j], items[index]
            helper(index + 1)
            items[index], items[j] = items[j], items[index]
    
    helper(0)
    return result</code></pre></div><p>The structure is very similar to the set-based version. The base case is when our index reaches the end of the array. This means that we have made a choice for every position and produced a complete permutation.</p><p>In the recursive step, we go through each item from the current index to the end. For each item, we swap it into the current position, call the function again with the next index, and then swap it back to restore the original order.</p><p>The best thing about this method is that we don't have to make copies of the available items at every step. We are directly changing the order of the elements in the array. In practice, this makes it a bit more efficient, even though the worst-case analysis stays the same: O(n! &#215; n) for time and O(n) for space.</p><h2>Permutations of Specific Length</h2><p>Sometimes we don&#8217;t need all the possible permutations of a set, but only those of a specific length. For example, if we have [1, 2, 3, 4], we could want all the two-length permutations: [1, 2], [1, 3], [1, 4], [2, 1], [2, 3], and so on.</p>
      <p>
          <a href="https://newsletter.francofernando.com/p/the-ordering-pattern-generating-all">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[How to Stop Failures from Spreading Between Services]]></title><description><![CDATA[Practical patterns to protect your services from failing dependencies and excessive load.]]></description><link>https://newsletter.francofernando.com/p/how-to-stop-failures-from-spreading</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/how-to-stop-failures-from-spreading</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 02 May 2026 08:43:03 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!r9Su!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0314d5d9-4c7b-4712-8619-7de2b23538db_950x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Welcome to the 171st issue of the Polymathic Engineer newsletter.</p><p>In one of our previous articles, we looked at the most <a href="https://newsletter.francofernando.com/p/why-distributed-systems-fail-and">common root causes of failures</a> in distributed systems and how to use redundancy and fault isolation to contain them. These are protections at the architectural level, which means they work based on how you design and deploy your system.</p><p>In this article, we are going to talk about something more tactical. We will discuss the patterns that stop faults from spreading from one service to another at runtime. These are techniques you can apply to existing systems with little effort.</p><p>The article is split into two parts. The first covers patterns that protect a service when one of its dependencies fails or slows down (downstream resiliency mechanisms). The second covers patterns that protect a service when its callers send more traffic than it can handle (upstream resiliency mechanisms).</p><p>The outline is as follows:</p><ul><li><p>Timeouts</p></li><li><p>Retries</p></li><li><p>Circuit breakers</p></li><li><p>Load shedding</p></li><li><p>Load leveling</p></li><li><p>Rate limiting</p></li><li><p>Constant work</p></li></ul><div><hr></div><p>Project-based learning is the best way to develop technical skills. <a href="https://app.codecrafters.io/join?via=FrancoFernando">CodeCrafters </a>is an excellent platform for tackling exciting projects, such as building your own Redis, Kafka, a <a href="https://app.codecrafters.io/join/dns-server?via=francofernando">DNS server</a>, SQLite, HTTP server or Git from scratch using your favourite programming language. Noe you can also try to build your own Claude Code (for free, still in beta).</p><p><a href="https://app.codecrafters.io/join?via=FrancoFernando">Sign up, and become a better software engineer</a>.</p><div><hr></div><h2>Timeouts</h2><p>When one service makes a network call, it should always set a timeout. The call fails if no response comes back within a certain amount of time. Without a timeout, the call might never return, and as we already discussed in the previous article, network calls that don&#8217;t return lead to resource leaks. Timeouts are the first protection you can use to find connectivity faults and stop them from cascading from one service to another.</p><p>You might think that setting timeouts is such a simple task that all network libraries do it for you, but that's not the case. When JavaScript's fetch API came out, there was no way to specify a timeout. Go's HTTP package doesn't employ timeouts by default, and the default timeout for the most popular Python library is infinity. Modern HTTP clients for Java and .NET do a better job and often come with default timeouts, but it's still a good idea to set them manually every time.  </p><p>There is a golden rule here: always set timeouts when making network calls, and keep a close eye on third-party libraries that make network calls but don&#8217;t let you configure a timeout. But how do you pick a good timeout value? </p><p>One method is to base it on how many false timeouts you are okay with. For example, if you don&#8217;t mind 0.1% of requests timing out even though they would have worked in the end, you can set the timeout according to the 99.9th percentile of the downstream service&#8217;s response time.</p><p>It is also important to have good monitoring in place to measure the full lifecycle of a network call. This allows you to check how long a network call took, what status code came back, and if it timed out. Without this kind of visibility at the points where your system is integrated, it's much harder to figure out what's wrong with production.</p><p>To avoid repeating the same timeout and monitoring logic everywhere, you can wrap the network call in a library that does both. Another option is a sidecar proxy running on the same machine. This proxy will intercept remote calls and take care of timeouts and monitoring for you.</p><h2>Retries</h2><p>When a network call fails or times out, the caller can either give up or try again. If the failure was caused by a short-lived connectivity issue, retrying after a short wait has a good chance of working. However, retrying immediately will make things worse if the downstream service is already too busy, </p><p>This is why retries should be slowed down. The most frequent strategy is exponential backoff, in which the delay between retries increases with each attempt. For example, if the initial wait is 2 seconds and you double it each time with an 8-second cap, the delays will be 2, 4, 8, 8, 8... seconds.</p><p>This is why retries need to be slowed down. The most common method is <strong>exponential backoff</strong>, in which the delay between retries increases with each attempt. For example, if the initial wait is 2 seconds and you double it each time with a cap at 8 seconds, the delays would be 2, 4, 8, 8, 8... seconds.</p><p>Exponential backoff is helpful, but it has a problem. When a downstream service goes down temporarily, many callers&#8217; requests will fail at the same time. They all retry on a similar schedule, which will cause the downstream service to experience load spikes that make things worse. To fix this, you can add some random jitter to the delay. This spreads out the retries over time and reduces the load on the struggling service.</p><p>Actively retrying right away isn&#8217;t always the only option, either. In batch systems that don&#8217;t need an immediate response, a failed request can be put into a retry queue. The same process, or a different one, can pick it up later and try again.</p><p>Additionally, not every failure is worth trying again. If the error isn&#8217;t temporary, such as the caller not being allowed to access the destination, retrying will fail again. If that is the case, the service should stop retrying right away. Also, if the network call is not idempotent, retrying can cause problems that affect the correctness of the application.</p><p>A last problem that is easy to overlook is <strong>retries amplification</strong>. Let&#8217;s suppose you have a chain of three services: the user calls service A, A calls service B, and B calls another service C. If the call from B to C fails, service B retries. But in the meantime, A sees a longer response time. If A times out, it retries too, and this adds even more load to the chain. If the user&#8217;s client also has retries, the total number of requests goes up quickly.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!r9Su!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0314d5d9-4c7b-4712-8619-7de2b23538db_950x306.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!r9Su!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0314d5d9-4c7b-4712-8619-7de2b23538db_950x306.png 424w, https://substackcdn.com/image/fetch/$s_!r9Su!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0314d5d9-4c7b-4712-8619-7de2b23538db_950x306.png 848w, https://substackcdn.com/image/fetch/$s_!r9Su!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0314d5d9-4c7b-4712-8619-7de2b23538db_950x306.png 1272w, https://substackcdn.com/image/fetch/$s_!r9Su!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0314d5d9-4c7b-4712-8619-7de2b23538db_950x306.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!r9Su!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0314d5d9-4c7b-4712-8619-7de2b23538db_950x306.png" width="589" height="189.72" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0314d5d9-4c7b-4712-8619-7de2b23538db_950x306.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:950,&quot;resizeWidth&quot;:589,&quot;bytes&quot;:210002,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/192933767?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0314d5d9-4c7b-4712-8619-7de2b23538db_950x306.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!r9Su!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0314d5d9-4c7b-4712-8619-7de2b23538db_950x306.png 424w, https://substackcdn.com/image/fetch/$s_!r9Su!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0314d5d9-4c7b-4712-8619-7de2b23538db_950x306.png 848w, https://substackcdn.com/image/fetch/$s_!r9Su!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0314d5d9-4c7b-4712-8619-7de2b23538db_950x306.png 1272w, https://substackcdn.com/image/fetch/$s_!r9Su!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0314d5d9-4c7b-4712-8619-7de2b23538db_950x306.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>This amplification effect puts more pressure on a service the further down the chain it is. If you have large dependence chains, it is safer to only retry at one level and fail fast at all the others.</p><h2>Circuit Breakers</h2><p>Timeouts help you find when a downstream service is slow or unreachable, and retries help you get past short-lived failures. But what if the failure doesn't go away quickly?</p>
      <p>
          <a href="https://newsletter.francofernando.com/p/how-to-stop-failures-from-spreading">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[How machine learning and optimization work well together]]></title><description><![CDATA[From making predictions to making choices.]]></description><link>https://newsletter.francofernando.com/p/how-machine-learning-and-optimization</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/how-machine-learning-and-optimization</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Wed, 29 Apr 2026 15:05:34 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/b7dec4b0-9dfb-4b48-ac2f-ac9623dcdb28_1440x686.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Today&#8217;s special issue features a guest post from Tim Varelmann, founder of <a href="https://bluebird-briefings.kit.com/polymathicengineer">Bluebird Optimization</a>. Tim is a reader of our newsletter. He contacted me to ask if there was an opportunity to collaborate on the newsletter as a guest author for an issue, and I gladly agreed because I think what he has to say could be interesting to you all.</p><p>Tim deve&#8230;</p>
      <p>
          <a href="https://newsletter.francofernando.com/p/how-machine-learning-and-optimization">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Linear Regression]]></title><description><![CDATA[How machines learn to draw a line through your data and make predictions.]]></description><link>https://newsletter.francofernando.com/p/linear-regression</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/linear-regression</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 25 Apr 2026 07:48:19 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/5b8798fd-c8b2-4401-8c30-8149b51fdb97_440x279.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Welcome to the 170th issue of the Polymathic Engineer newsletter.</p><p>Linear regression is a fundamental algorithm in machine learning. Even though it is simple and intuitive, many engineer treat it as a black box. They call a function from scikit-learn, get a result, and move on.</p><p>But understanding what happens under the hood is critical because linear regression is the basis for more complicated models like neural networks. In this issue, we break down linear regression from the ground up. </p><p>We start with a simple example, build an intuition for how it works, and then explore the math and algorithms that make it all possible. The outline is as follows:</p><ul><li><p>What is Linear Regression?</p></li><li><p>Building an Intuition: Predicting Salaries</p></li><li><p>Features, Weights, and Bias</p></li><li><p>How to Find the Best Line?</p></li><li><p>Measuring How Good a Model Is: Error Functions</p></li><li><p>From One Feature to Many: Multivariate Linear Regression</p></li><li><p>Polynomial Regression</p></li><li><p>Parameters vs Hyperparameters</p></li><li><p>Real-World Applications</p></li><li><p>Putting Everything Into Practice</p></li></ul><p>Regardless of whether you are an experienced engineer who wants to brush up on the basics or a younger engineer exploring machine learning for the first time, this article will give you a solid understanding of how linear regression works and why it matters.</p><div><hr></div><p>Project-based learning is the best way to develop technical skills. <a href="https://app.codecrafters.io/join?via=FrancoFernando">CodeCrafters </a>is an excellent platform for tackling exciting projects, such as building your own Redis, Kafka, a <a href="https://app.codecrafters.io/join/dns-server?via=francofernando">DNS server</a>, SQLite, HTTP server or Git from scratch using your favourite programming language. Noe you can also try to build your own Claude Code (for free, still in beta). </p><p><a href="https://app.codecrafters.io/join?via=FrancoFernando">Sign up, and become a better software engineer</a>.</p><div><hr></div><h2>What is Linear Regression?</h2><p>Let&#8217;s start with the basics. Say you have a set of data points that roughly look like they are forming a line. The goal of linear regression is to draw the line that passes as close as possible to all of them.</p><p>Think of the data points as houses in a town, and your goal is to build a road that goes through the town. Everyone wants to live near the road, and your job is to make them as happy as possible. The road should go as close as possible to all the homes.</p><p>Of course, this leads to some questions like</p><ul><li><p>What do we mean by &#8220;points that roughly form a line&#8221;?</p></li><li><p>What do we mean by &#8220;a line that passes really close to the points&#8221;?</p></li><li><p>How do we find such a line?</p></li><li><p>Why is this useful in the real world?</p></li></ul><p>We answer all these questions in the following sections.</p><h2>Building an Intuition: Predicting Salaries</h2><p>Let&#8217;s suppose that you want to guess a software engineer&#8217;s salary based on their years of experience. You get some information from a few engineers and get something like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4dT7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1716631b-df7f-49bb-bc5a-dadbb4511e46_1169x455.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4dT7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1716631b-df7f-49bb-bc5a-dadbb4511e46_1169x455.png 424w, https://substackcdn.com/image/fetch/$s_!4dT7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1716631b-df7f-49bb-bc5a-dadbb4511e46_1169x455.png 848w, https://substackcdn.com/image/fetch/$s_!4dT7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1716631b-df7f-49bb-bc5a-dadbb4511e46_1169x455.png 1272w, https://substackcdn.com/image/fetch/$s_!4dT7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1716631b-df7f-49bb-bc5a-dadbb4511e46_1169x455.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4dT7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1716631b-df7f-49bb-bc5a-dadbb4511e46_1169x455.png" width="1169" height="455" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1716631b-df7f-49bb-bc5a-dadbb4511e46_1169x455.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:455,&quot;width&quot;:1169,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:65678,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/190020077?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1716631b-df7f-49bb-bc5a-dadbb4511e46_1169x455.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4dT7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1716631b-df7f-49bb-bc5a-dadbb4511e46_1169x455.png 424w, https://substackcdn.com/image/fetch/$s_!4dT7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1716631b-df7f-49bb-bc5a-dadbb4511e46_1169x455.png 848w, https://substackcdn.com/image/fetch/$s_!4dT7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1716631b-df7f-49bb-bc5a-dadbb4511e46_1169x455.png 1272w, https://substackcdn.com/image/fetch/$s_!4dT7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1716631b-df7f-49bb-bc5a-dadbb4511e46_1169x455.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>If you plot the data on a coordinate system, with years of experience on the x-axis and salary on the y-axis, you see that the dots make a line that goes up. The more you are experienced, the more money you make (we all know it doesn&#8217;t work exactly like that, but let&#8217;s pretend for the sake of this example).</p><p>Now, let&#8217;s say that a new engineer starts working for your company. She has 4.5 years of experience and you need to make her an offer. There isn&#8217;t a data point for 4.5 years, but you can do it another way: you can find out where 4.5 falls on the line and read the salary. That is linear regression in a nutshell.</p><p>What the model actually learns is a simple equation:</p><p><em>Salary = weight &#215; (years of experience) + bias</em></p><p>The weight shows you how much the salary goes up for each year of experience. The bias is the starting point: starting salary with zero experience. We will get into what these two terms mean in detail in the next section.</p><h2>Features, Weights, and Bias</h2><p>Let&#8217;s have a look at the building blocks of linear regression. There are four things you need to know:</p><ul><li><p><strong>Features: </strong>they are the inputs we use to make a prediction. In our example, years of experience are the feature. But in a more complex model, we could have many features: programming languages known, location, company size, and so on.</p></li><li><p><strong>Label: </strong>they are what we are trying to guess. In our case, the label is the salary.</p></li><li><p><strong>Weights: </strong>they tell the model how much each feature matters. Going back to our equation, the weight might be something like 6,500, since for every extra year of experience, the salary increases by that amount. If the weight was negative, it would mean that more experience is associated with a lower salary, which would be strange. A weight close to zero means the feature barely matters. If you added a feature like <em>the number of monitors </em>on the desk and its weight turned out to be near zero, the model is telling you it doesn&#8217;t help guess the salary.</p></li><li><p><strong>Bias: </strong>it is the starting point. It is the salary the model would guess if the engineer had zero years of experience. In practice, the bias is just there to give the line the flexibility to sit at the right height on the graph.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aBLS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d57eb4-61f3-4cd1-92ed-3c6905ee9f0b_402x271.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aBLS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d57eb4-61f3-4cd1-92ed-3c6905ee9f0b_402x271.png 424w, https://substackcdn.com/image/fetch/$s_!aBLS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d57eb4-61f3-4cd1-92ed-3c6905ee9f0b_402x271.png 848w, https://substackcdn.com/image/fetch/$s_!aBLS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d57eb4-61f3-4cd1-92ed-3c6905ee9f0b_402x271.png 1272w, https://substackcdn.com/image/fetch/$s_!aBLS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d57eb4-61f3-4cd1-92ed-3c6905ee9f0b_402x271.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aBLS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d57eb4-61f3-4cd1-92ed-3c6905ee9f0b_402x271.png" width="402" height="271" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b0d57eb4-61f3-4cd1-92ed-3c6905ee9f0b_402x271.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:271,&quot;width&quot;:402,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:22499,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/190020077?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d57eb4-61f3-4cd1-92ed-3c6905ee9f0b_402x271.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!aBLS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d57eb4-61f3-4cd1-92ed-3c6905ee9f0b_402x271.png 424w, https://substackcdn.com/image/fetch/$s_!aBLS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d57eb4-61f3-4cd1-92ed-3c6905ee9f0b_402x271.png 848w, https://substackcdn.com/image/fetch/$s_!aBLS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d57eb4-61f3-4cd1-92ed-3c6905ee9f0b_402x271.png 1272w, https://substackcdn.com/image/fetch/$s_!aBLS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d57eb4-61f3-4cd1-92ed-3c6905ee9f0b_402x271.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In other words, the model finds the best weights and bias values that make the line fit the data. That&#8217;s the whole process of learning. We&#8217;ll talk about how it does that in the next part.</p><h2>How To Find the Best Line?</h2><p>You could draw an endless number of lines through a set of data points. The question is: which one is the best? The answer is the line that is as close as possible to all data points. But we need to be more precise. Here, &#8220;Close to all the points&#8221; means the total distance between the line and the data points is as small as possible. </p><p>In this context, the distance between what the model predicts and what the data point says is called an <strong>error </strong>or <strong>residual</strong>. There are two main ways to find this line.</p>
      <p>
          <a href="https://newsletter.francofernando.com/p/linear-regression">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Finding All Combinations with the The Selection Pattern]]></title><description><![CDATA[How to recursively explore every possible subset using include/exclude decisions.]]></description><link>https://newsletter.francofernando.com/p/finding-all-combinations-with-the</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/finding-all-combinations-with-the</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 18 Apr 2026 12:47:44 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!NUaz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff94781-9396-411a-ba37-42998198c514_1082x403.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Welcome to the 169th issue of the Polymathic Engineer newsletter. This week, we continue our recursion series with the third article.</p><p>In the previous two articles, we discussed the <a href="https://newsletter.francofernando.com/p/mastering-recursion-the-foundations">basics of recursion</a> and looked at the first two patterns: <a href="https://newsletter.francofernando.com/p/recursion-in-practice-iteration-and">Iteration and Subproblems</a>. While these patterns are useful, they don&#8217;t go very deep. Now we move on to a stronger pattern: Selection.</p><p>Selection is arguably the most important recursive pattern you can learn. It frequently comes up in coding interviews, and it is the basis for dynamic programming. A lot of problems that seem hard at first become easy once you realize that they are selection problems in disguise.</p><p>The core concept is easy to understand. You look at a set of items and decide whether or not to include each one in your result. By exploring every possible combination of these choices, you generate every possible subset of the original set. From there, you can filter, count, or optimize to get the answer you need.</p><p>This article will explore how this pattern works and how to apply it to real problems.</p><p>The outline is as follows:</p><ul><li><p>The core selection pattern</p></li><li><p>Counting combinations</p></li><li><p>Generating Combinations</p></li><li><p>Combinations of a specific length</p></li><li><p>The 0/1 Knapsack problem</p></li><li><p>When selection applies</p></li></ul><div><hr></div><p>Project-based learning is the best way to develop technical skills. <a href="https://app.codecrafters.io/join?via=FrancoFernando">CodeCrafters </a>is an excellent platform for tackling exciting projects, such as building your own Redis, Kafka, a <a href="https://app.codecrafters.io/join/dns-server?via=francofernando">DNS server</a>, SQLite, or Git from scratch. <a href="https://app.codecrafters.io/join?via=FrancoFernando">Sign up, and become a better software engineer</a>.</p><div><hr></div><h2>The Core Selection Pattern</h2><p>The selection pattern answers a specific type of question: &#8220;Can this problem be solved by finding all possible combinations?&#8221; If the answer is yes, then you can use the same approach every time.</p><p>The starting point is a set of items. For each item, you have two options: include it in the result or leave it out. You go through the items one by one, and at every step, you branch into two paths. One includes the current item, and the other path excludes it.</p><p>As a concrete example, consider the array [1, 2, 3]. You start at the first item. You can either include 1 or exclude it. If you include it, your partial result is [1]. If you exclude it, your partial result is [].</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NUaz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff94781-9396-411a-ba37-42998198c514_1082x403.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NUaz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff94781-9396-411a-ba37-42998198c514_1082x403.png 424w, https://substackcdn.com/image/fetch/$s_!NUaz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff94781-9396-411a-ba37-42998198c514_1082x403.png 848w, https://substackcdn.com/image/fetch/$s_!NUaz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff94781-9396-411a-ba37-42998198c514_1082x403.png 1272w, https://substackcdn.com/image/fetch/$s_!NUaz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff94781-9396-411a-ba37-42998198c514_1082x403.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NUaz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff94781-9396-411a-ba37-42998198c514_1082x403.png" width="1082" height="403" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/aff94781-9396-411a-ba37-42998198c514_1082x403.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:403,&quot;width&quot;:1082,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:111702,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/193139411?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff94781-9396-411a-ba37-42998198c514_1082x403.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NUaz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff94781-9396-411a-ba37-42998198c514_1082x403.png 424w, https://substackcdn.com/image/fetch/$s_!NUaz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff94781-9396-411a-ba37-42998198c514_1082x403.png 848w, https://substackcdn.com/image/fetch/$s_!NUaz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff94781-9396-411a-ba37-42998198c514_1082x403.png 1272w, https://substackcdn.com/image/fetch/$s_!NUaz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff94781-9396-411a-ba37-42998198c514_1082x403.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now you move to the second item. For each of the two partial results, you again have two choices. Include or exclude 2. This gives you four partial results: [1, 2], [1], [2], and []. You continue this process for the third item. Each of the four results branches into two, and you end up with eight total combinations: [1, 2, 3], [1, 2], [1, 3], [1], [2, 3], [2], [3], and []. This is the complete set of subsets. </p><p>There are always 2^n subsets for an array of n elements, including the empty set. The selection pattern generates all of them by examining every possible path through the decision tree.</p><p>The word &#8220;selection&#8221; comes from the choice you make at each step. You are selecting which items to include in your result. People also call this the "combinations" pattern, but I prefer "selection" because it captures what you do: you either choose the item, or you don't.</p><h2>Counting Combinations</h2><p>Before looking at how to generate all the combinations, it helps to start with a simpler task: counting how many combinations exist. This is a good approach since it lets you focus on the recursive structure without worrying about building up results.</p><p>The logic is straightforward. You return 1 if you have reached the end of the array and discovered one valid combination. Otherwise, you make two recursive calls, one which includes the current item and one that excludes it. You add the results together.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2bd45dfb-ae69-4d6e-8f7d-2f6223b71780&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def count_combinations(arr, i=0):
    if i == len(arr):
        return 1

    include = count_combinations(arr, i + 1)
    exclude = count_combinations(arr, i + 1)

    return include + exclude</code></pre></div><p>Let&#8217;s trace what happens when we run this code with the array [1, 2, 3]. The initial call branches into two: one where we include 1 and one where we don't. Then we move to item 2, and each of those branches splits into two other branches. The same happens for item 3. When we reach the end of the array, we return 1. All those 1s add up as the calls return, and we get 8 at the top.</p><p>You might notice that the include and exclude calls are identical. That is because the number of remaining combinations stays the same regardless of whether you include an item. There are always the same number of paths forward. This will not hold when we add constraints.</p><p>The time complexity is O(2^n) because we make two recursive calls at each level, and the depth is n. The space complexity is O(n) because of the call stack. These numbers match what we expect: there are 2^n combinations, and we visit each one.</p><h2>Generating Combinations</h2><p>Counting is straightforward, but for most problems, you need to actually generate the combinations. This is where things get more interesting. There are two main ways to do this: either build up the results as you return them, or update a passed variable as you go.</p>
      <p>
          <a href="https://newsletter.francofernando.com/p/finding-all-combinations-with-the">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[The Dictionary Problem: Fast Lookups in Large Collections]]></title><description><![CDATA[From simple arrays to hash tables and BSTs: exploring the trade-offs behind one of the most common problems in computer science.]]></description><link>https://newsletter.francofernando.com/p/the-dictionary-problem-fast-lookups</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/the-dictionary-problem-fast-lookups</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 11 Apr 2026 09:32:49 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/0eea31a5-227a-48a7-b58d-db9a23cbec64_814x412.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Welcome to the 168th issue of the Polymathic Engineer newsletter.</p><p>Let&#8217;s say you are building a spell checker that runs directly in the browser. You have a dictionary of over 100,000 words, and every time the user types something, you need to quickly check if the word exists in your dictionary. If it doesn&#8217;t, you underline it in red.</p><p>The feature&#8230;</p>
      <p>
          <a href="https://newsletter.francofernando.com/p/the-dictionary-problem-fast-lookups">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Why Distributed Systems Fail and How to Limit the Damage]]></title><description><![CDATA[A look at the most common failure modes and the techniques to reduce their impact.]]></description><link>https://newsletter.francofernando.com/p/why-distributed-systems-fail-and</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/why-distributed-systems-fail-and</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 04 Apr 2026 05:30:37 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!uJXx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f1391e2-e82d-49f3-af33-ab5be5edb902_645x395.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Welcome to the 167th issue of the Polymathic Engineer.</p><p>A rule of thumb when working on a distributed system is to assume that anything can go wrong. The way engineers usually deal with this is by using scalability patterns like data partitioning, functional decomposition, and replication.</p><p>The problem with these patterns is that they all add more moving parts and make our systems more complicated. Since each part has a chance of failing, the more there are, the higher the chance that at least one of them will fail at any given moment. Software crashes, power outages, hardware faults, and memory leaks can happen all the time.</p><p>This isn't only a worry in theory. As we discussed in <a href="https://francofernando.substack.com/p/availability">another article</a>, a system can only be down for approximately 15 minutes a day to guarantee it is available 99.99% of the time. For three nines, that drops to 43 minutes per month. In general, the more nines you want, the faster your system needs to detect failures and recover from them when they occur. There is no time for a person to notice anything went wrong and fix it.</p><p>In this article, we will discuss the most common root causes of failures in distributed systems and see how to address them. The outline is as follows:</p><ul><li><p>Common root causes of failures</p></li><li><p>Managing risk</p></li><li><p>Redundancy</p></li><li><p>Correlation</p></li><li><p>Fault isolation</p></li><li><p>Shuffle Sharding and Cellular Architecture</p></li></ul><div><hr></div><p>Project-based learning is the best way to develop technical skills. <a href="https://app.codecrafters.io/join?via=FrancoFernando">CodeCrafters </a>is an excellent platform for tackling exciting projects, such as building your own Redis, Kafka, a <a href="https://app.codecrafters.io/join/dns-server?via=francofernando">DNS server</a>, SQLite, or Git from scratch. <a href="https://app.codecrafters.io/join?via=FrancoFernando">Sign up, and become a better software engineer</a>.</p><div><hr></div><h2>Common root causes of failures</h2><p>To build fault-tolerant distributed systems, you first need to fully understand what can go wrong. Let's start looking at the most common failure modes you can encounter.</p><h4>Infrastructure Faults</h4><p>Hardware doesn&#8217;t last forever. Hard drives fail, memory modules go bad, and network cards stop working. Power outages happen, and fiber cuts or electrical storms can take down whole data centers. The good news is that redundancy makes it straightforward to remedy these infrastructural problems. The underlying idea is simple: if one of your servers goes down, the others may take over.</p><h4>Incorrect Error Handling </h4><p>A <a href="https://scispace.com/pdf/an-analysis-of-production-failures-in-distributed-data-igrujmovmy.pdf">study of user-reported failures</a> from well-known distributed data storage showed an unexpected result. Most major failures were not caused by infrastructure issues. They were caused by mistakes made when dealing with non-fatal faults. The bugs were very easy to fix. In some cases, error handlers didn&#8217;t even look at errors; in others, handlers were only partially set up, or caught too generic exceptions. The main point was that most bugs could have been found with simple unit tests.</p><h4>Configuration Changes </h4><p>Changes to configurations cause failures more often than most engineers think, and it is not always a simple mistake like a typo in a database connection string that makes them risky. One example is a valid configuration change turning on a feature flag that hasn&#8217;t been used in years. Even if there is a code path, and the value looks correct, the behavior is no longer what was meant. </p><p>Configuration changes are challenging because they may not take effect right away. A change that is not valid might not be seen for hours or days if an application only gets a configuration value when a certain code path runs. This is why configuration should be managed like code: version-controlled, reviewed, and released incrementally.</p><h4>Single points of failure</h4><p>A single point of failure is any component that, brings the entire system down when it fails. The tricky part is that they are not always obvious. </p><p>People are a surprisingly common one. If someone has to manually execute a sequence of operational steps in the right order without making a mistake, it is only a matter of time before something goes wrong. Computers, on the other hand, are much better at executing instructions. This is why you should automate whenever possible.</p><p>But systems have non-human SPOFs as well. DNS is a good example: if clients can&#8217;t resolve your domain, it doesn&#8217;t matter how healthy your servers are. TLS certificates are another one: if yours expires, every client connection will be refused. </p><p>When designing a system, a useful exercise is to go through each component and ask what happens if it disappears. Some SPOFs can be removed with redundancy. Others can&#8217;t, but you can at least work to reduce how much damage they cause when they do fail.</p><h4>Network faults</h4><p>When two processes talk to each other over the network, a lot can go wrong. Messages might get lost, arrive late, or be sent more than once. The sender rarely knows which of these things happened because they only see a timeout. </p><p>But timeouts are the easy case. A more difficult situation is when the network doesn&#8217;t completely fail; it just gets slow. A process sends a request and gets a response, but it takes 10 times longer than usual. There is no error to catch and no timeout to trigger. The caller just waits, hanging on to resources while they do. These partial failures are sometimes called <em>gray failures</em> and are considerably harder to find than a clean crash. A slow dependency can silently bring the entire system to a halt before anybody knows something is wrong.</p><h4>Resource leaks</h4><p>Every process has a limited number of resources it can use, such as memory, threads, file handles, and database connections. When a bug causes one of these resources not to be released properly, you get a leak. The hard part is that leaks don&#8217;t show up right away. A slow memory leak might take days to fill up the heap. A connection pool that loses one connection per hour won&#8217;t cause problems until there are none left.</p><p>This is what makes resource leaks so dangerous: they pass all the tests in staging but don't show up in production until the app has been running long enough. And when they do ultimately fix the problem, it often doesn't seem like the original defect at all. A leaky connection doesn't make a sound, but it just shows up as a strange timeout in another part of the system.</p><h4>Load pressure</h4><p>A system can perform perfectly fine under normal conditions, but it can break down when the load gets too high. The hard aspect is that the extra load doesn't necessarily come from the users. Sometimes the spike comes from places you don&#8217;t expect: a web scraper hitting your API thousands of times per second, or a denial-of-service attack flooding your servers with traffic.</p><p>What makes load-related failures difficult is that they can turn a healthy system into an unhealthy one very quickly. When the thread pool is full, a service that responds in 50 milliseconds under normal load can take 5 seconds. And that slowdown does not stay in one place, but extends to all the other services that are waiting for a response.</p><h4>Cascading failures</h4><p>Cascading failures happen when a fault in a component causes faults in some others, which then trigger more faults, and so on. A common example is when a server gets too many requests, starts to answer slowly, and callers start to time out. Those callers retry, adding even more load to the already struggling server. The situation gets worse and worse until the whole system grinds to a halt.</p><p><em>Metastable failures</em> are the most dangerous kind of cascade failures. This happens when the system gets stuck in a bad state that doesn't go away even after the original cause has gone away. For example, a short spike in traffic creates a lot of retries. The retries themselves create enough load to produce more retries, and the system persists in this self-sustaining failure mode long after the original spike is over. When this happens, it is not possible to just wait for the problem to pass. The system needs an intervention to break the cycle.</p><h2>Managing Risk</h2><p>By now, the list of things that can go wrong may seem a bit too long. But just because a mistake could happen doesn't imply we have to fix it immediately.</p>
      <p>
          <a href="https://newsletter.francofernando.com/p/why-distributed-systems-fail-and">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[What Really Makes a Succesful Software Engineer]]></title><description><![CDATA[Five skills no course teach you, but matter more than your tech stack.]]></description><link>https://newsletter.francofernando.com/p/what-really-makes-a-succesful-software</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/what-really-makes-a-succesful-software</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 28 Mar 2026 11:31:29 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Gb7G!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53db50e-3d1d-4dff-a350-87a2860212bb_1254x508.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Gb7G!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53db50e-3d1d-4dff-a350-87a2860212bb_1254x508.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Gb7G!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53db50e-3d1d-4dff-a350-87a2860212bb_1254x508.png 424w, https://substackcdn.com/image/fetch/$s_!Gb7G!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53db50e-3d1d-4dff-a350-87a2860212bb_1254x508.png 848w, https://substackcdn.com/image/fetch/$s_!Gb7G!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53db50e-3d1d-4dff-a350-87a2860212bb_1254x508.png 1272w, https://substackcdn.com/image/fetch/$s_!Gb7G!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53db50e-3d1d-4dff-a350-87a2860212bb_1254x508.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Gb7G!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53db50e-3d1d-4dff-a350-87a2860212bb_1254x508.png" width="1254" height="508" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f53db50e-3d1d-4dff-a350-87a2860212bb_1254x508.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:508,&quot;width&quot;:1254,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:332980,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/192201133?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53db50e-3d1d-4dff-a350-87a2860212bb_1254x508.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Gb7G!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53db50e-3d1d-4dff-a350-87a2860212bb_1254x508.png 424w, https://substackcdn.com/image/fetch/$s_!Gb7G!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53db50e-3d1d-4dff-a350-87a2860212bb_1254x508.png 848w, https://substackcdn.com/image/fetch/$s_!Gb7G!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53db50e-3d1d-4dff-a350-87a2860212bb_1254x508.png 1272w, https://substackcdn.com/image/fetch/$s_!Gb7G!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53db50e-3d1d-4dff-a350-87a2860212bb_1254x508.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Hi Friends,</p><p>Welcome to the 166th issue of the Polymathic Engineer newsletter.</p><p>When you think about becoming a better software engineer, your mind probably goes straight to technical skills. Learning another framework or mastering a programming language, or practicing system design patterns. And yes, these things matter because they are what get you throug&#8230;</p>
      <p>
          <a href="https://newsletter.francofernando.com/p/what-really-makes-a-succesful-software">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Recursion in Practice: Iteration and Subproblems ]]></title><description><![CDATA[How to apply the first two recursive patterns to real problems.]]></description><link>https://newsletter.francofernando.com/p/recursion-in-practice-iteration-and</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/recursion-in-practice-iteration-and</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 21 Mar 2026 09:32:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!HcFg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e94954f-53d6-42d3-ace9-615351e76822_448x443.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends, </p><p>Welcome to the 165th issue of the Polymathic Engineer. This week, we keep going our recursion series with the second article.</p><p>In the <a href="https://newsletter.francofernando.com/p/mastering-recursion-the-foundations">previous article</a>, we covered the foundations: base cases, recursive steps, return strategies, tracing code, and complexity analysis. Now it&#8217;s time to put those basics to work.</p><p>This article covers the first two recursive patterns: Iteration and Subproblems. These are the most approachable patterns and a natural starting point before tackling all the others.</p><p>The outline is as follows:</p><ul><li><p>The Iteration pattern</p></li><li><p>Iterating forward and backward over arrays</p></li><li><p>Inserting at the bottom of a stack</p></li><li><p>Nested iteration with recursion</p></li><li><p>The Subproblems pattern</p></li><li><p>Towers of Hanoi</p></li><li><p>Checking if a string is a palindrome</p></li><li><p>The stair step problem</p></li></ul><div><hr></div><p>Project-based learning is the best way to develop technical skills. <a href="https://app.codecrafters.io/join?via=FrancoFernando">CodeCrafters </a>is an excellent platform for tackling exciting projects, such as building your own Redis, Kafka, a <a href="https://app.codecrafters.io/join/dns-server?via=francofernando">DNS server</a>, SQLite, or Git from scratch. <a href="https://app.codecrafters.io/join?via=FrancoFernando">Sign up, and become a better software engineer</a>.</p><div><hr></div><h2>The Iteration Pattern</h2><p>The iteration pattern is the simplest of the six. The basic idea is to replace a for loop with a recursive function. Each recursive call handles a step of the iteration, and the base case is when there are no more elements to process.</p><p>At first, this may seem like a waste of time. Why change a for loop that already works to recursion? There are a few times when it makes sense.</p><p>The first is when you need to access data in reverse order. Consider printing a linked list backward. With iteration, you would have to push all the values onto a stack and then pop them off. With recursion, the call stack does this for you automatically. The code becomes shorter and easier to follow.</p><p>The second is when the recursion takes care of memory for you. Some problems need temporary storage that matches the structure of the input. You don&#8217;t have to explicitly create and manage that storage yourself; instead, you can let the recursive calls hold the data in their stack frames. We will see a concrete example of this when we insert an element at the bottom of a stack.</p><p>The third is when you use functional programming. Languages like Haskell or certain styles of JavaScript avoid mutable state entirely. Recursion takes the place of loops in these contexts because there is no loop that doesn&#8217;t change a counter variable.</p><p>There is one thing to keep in mind: the iteration pattern doesn&#8217;t usually improve time complexity. Most of the time, the recursive version does the same amount of work as the iterative one. The advantage is that the code is cleaner, not that it runs faster. And since recursion uses stack frames, it can actually use more memory than a simple loop.</p><p>When you use this pattern, there are two questions to ask yourself. The first is: what is the iterative step? In a regular for loop, this might be i + 1, or i + 2, or something more complex. Your recursive call will mirror this. The second is: which way do you want to go? Going forward or backward? The answer affects where you put the recursive call relative to the work you do in each step. We'll see both directions in the next section.</p><h2>Iterating Forward and Backward</h2><p>Let&#8217;s start with a simple example: print every element in an array. Here is the iterative version (I know the code is not pythonic but it is better for the sake of the examples):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7ef6ba97-325a-49e9-9b78-2319af186948&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def print_array(arr):

    for i in range(len(arr)):

        print(arr[i])</code></pre></div><p>To make this code recursive, we need to think about what happens at each step of the iteration. In this case, we move from index i to index i + 1. The base case occurs when the index reaches the array's length, which means that there are no more elements to print. The recursive version does the same. It prints the current element, then moves to the next. When it runs out of elements, it stops.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;265093d2-80c9-4985-857c-d4c967026252&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def print_array(arr, i=0):
    if i == len(arr):
        return

    print(arr[i])
    print_array(arr, i + 1)</code></pre></div><p>Now here is something interesting. If you swap the order of the last two lines, you get the array printed in reverse:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c975c189-af3d-4cb1-b11a-35b03ae2c1e6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def print_array_reverse(arr, i=0):
    if i == len(arr):
        return

    print_array_reverse(arr, i + 1)
    print(arr[i])</code></pre></div><p>The only change is that the print statement comes after the recursive call. This means we first go to the end of the array, then print the elements as the calls come back. The last element in the array is printed first, and so on.</p><p>This is a pattern you will see often: the order of the recursive call relative to the work determines the direction of processing. If you do the work first and then recurse, you process forward. If you recurse first and then do the work, you process backward.</p><p>You can use the same idea when you want to return a result rather than print it. As we discussed in the first article, you can either use a passed variable or build up the result as you return. Here is an example that sums all the elements in an array with the build-up approach:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a1ac03a4-f360-4576-a2b6-e412338b21e9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def sum_array(arr, i=0):
    if i == len(arr):
        return 0

    return arr[i] + sum_array(arr, i + 1)</code></pre></div><p>The function finds the sum from index i to the end. The base case says that the sum of an empty slice is 0. The recursive step states that the sum is the current element plus the sum of everything after it. As the calls return, the values add up.</p><p>If you prefer using a passed variable, the code looks like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;04eae0cd-1c94-479e-975a-a4f468ba8136&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def sum_array(arr, i=0, result=None):
    if result is None:
        result = [0]

    if i == len(arr):
        return result[0]

    result[0] += arr[i]
    return sum_array(arr, i + 1, result)</code></pre></div><p>Both approaches lead to the same answer. The build-up version is usually cleaner for simple cases, while the passed-variable can be better as the logic gets more complex.</p><h2>Inserting at the Bottom of a Stack</h2><p>Now, let&#8217;s look at a problem where recursion does more than just take the place of a for loop. You have to put something at the bottom of a stack without using any extra data structures.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HcFg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e94954f-53d6-42d3-ace9-615351e76822_448x443.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HcFg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e94954f-53d6-42d3-ace9-615351e76822_448x443.png 424w, https://substackcdn.com/image/fetch/$s_!HcFg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e94954f-53d6-42d3-ace9-615351e76822_448x443.png 848w, https://substackcdn.com/image/fetch/$s_!HcFg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e94954f-53d6-42d3-ace9-615351e76822_448x443.png 1272w, https://substackcdn.com/image/fetch/$s_!HcFg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e94954f-53d6-42d3-ace9-615351e76822_448x443.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HcFg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e94954f-53d6-42d3-ace9-615351e76822_448x443.png" width="244" height="241.27678571428572" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9e94954f-53d6-42d3-ace9-615351e76822_448x443.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:443,&quot;width&quot;:448,&quot;resizeWidth&quot;:244,&quot;bytes&quot;:134067,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.francofernando.com/i/189814178?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e94954f-53d6-42d3-ace9-615351e76822_448x443.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!HcFg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e94954f-53d6-42d3-ace9-615351e76822_448x443.png 424w, https://substackcdn.com/image/fetch/$s_!HcFg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e94954f-53d6-42d3-ace9-615351e76822_448x443.png 848w, https://substackcdn.com/image/fetch/$s_!HcFg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e94954f-53d6-42d3-ace9-615351e76822_448x443.png 1272w, https://substackcdn.com/image/fetch/$s_!HcFg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e94954f-53d6-42d3-ace9-615351e76822_448x443.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>If you try to do this iteratively, you run into a problem. You can only access the top of a stack, so you have to pop everything off, insert your item, and push everything back on. This requires a temporary stack to hold the elements (In Python, we use a list as a stack, with append() to push and pop() to remove from the top):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;86348a9e-417a-4d32-ad2c-7ebf2d67b8f0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def insert_at_bottom_iterative(stack, item):
    temp = []
    while stack:
        temp.append(stack.pop())

    stack.append(item)
    while temp:
        stack.append(temp.pop())</code></pre></div><p>The code works, but you need to create and maintain the temporary stack. When you use recursion, the call stack takes care of this implicitly. The recursive approach pops elements off one at a time, makes a recursive call, and then pushes each element back on after the call comes back. The base case is when the stack is empty, at which point we insert the item. As the recursive calls return, each previously popped element gets pushed back on top:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f8aeb9db-74ff-4f60-b17d-369335e05f30&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def insert_at_bottom(stack, item):
    if not stack:
        stack.append(item)
        return

    top = stack.pop()
    insert_at_bottom(stack, item)
    stack.append(top)</code></pre></div><p>Let&#8217;s trace through this with a concrete example. Suppose the stack is [1, 2, 3, 4] (with 4 on top) and we want to insert 5 at the bottom.</p><p>The first call pops 4 and saves it. The stack is now [1, 2, 3]. The second call pops 3. The stack is [1, 2]. The third call pops 2. The stack is [1]. The fourth call pops 1, leaving the stack empty.</p><p>Now we hit the base case. We append 5, so the stack is [5]. Then the calls start coming back. The fourth call appends 1, giving us [5, 1]. The third call appends 2, giving us [5, 1, 2]. The second call appends 3, giving us [5, 1, 2, 3]. The first call appends 4, and we end up with [5, 1, 2, 3, 4].</p><p>The time complexity is O(n) because we need to pop and push each element once. The space complexity is also O(n) since the call stack must hold all popped elements. This is the same as the iterative version, but the recursive code is shorter and easier to read.</p><h2>Nested Iteration with Recursion</h2>
      <p>
          <a href="https://newsletter.francofernando.com/p/recursion-in-practice-iteration-and">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Mastering Recursion: the Foundations]]></title><description><![CDATA[Understanding when to use recursion, how to structure recursive functions, and how to go through any recursive code.]]></description><link>https://newsletter.francofernando.com/p/mastering-recursion-the-foundations</link><guid isPermaLink="false">https://newsletter.francofernando.com/p/mastering-recursion-the-foundations</guid><dc:creator><![CDATA[Franco Fernando]]></dc:creator><pubDate>Sat, 14 Mar 2026 07:31:04 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!BbZc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faefe698d-ed70-48d8-ae3f-e6884e0f6f92_832x379.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Friends,</p><p>Welcome to the 164th issue of the Polymathic Engineer newsletter.</p><p>Recursion is one of those topics that many software engineers &#8220;understand&#8221; in theory but struggle with in practice. They know the textbook definition (a function that calls itself). They have seen the Fibonacci example multiple times. But when they are asked to solve a recursive problem in an interview, something doesn&#8217;t work.</p><p>The problem is not that they are not smart, but that they don&#8217;t have the correct mental models. It took me a while to figure this out. When someone explained it to me, I was able to follow recursive code, but writing it from scratch often felt like guessing. The turning point was when I stopped thinking of recursion as a single, isolated technique and began seeing it as a set of patterns, each with its own structure.</p><p>In this article, we will start building the basic mental models you need to actually use recursion. In future articles, we will apply these foundations to specific patterns and problems.</p><p>The outline is as follows:</p><ul><li><p>When to use recursion</p></li><li><p>The two building blocks: base case and recursive step</p></li><li><p>Three ways to return results from recursive functions</p></li><li><p>How to trace any recursive code (even when you&#8217;re lost)</p></li><li><p>Time and space complexity for recursion</p></li><li><p>A preview of the six recursive patterns</p></li></ul><div><hr></div><p>Project-based learning is the best way to develop technical skills. <a href="https://app.codecrafters.io/join?via=FrancoFernando">CodeCrafters </a>is an excellent platform for tackling exciting projects, such as building your own Redis, Kafka, a <a href="https://app.codecrafters.io/join/dns-server?via=francofernando">DNS server</a>, SQLite, or Git from scratch. <a href="https://app.codecrafters.io/join?via=FrancoFernando">Sign up, and become a better software engineer</a>.</p><div><hr></div><h2>When to Use Recursion</h2><p>There is a fact about recursion that people often overlook: any problem that can be solved recursively can also be solved iteratively. There is no case where you need to use recursion.</p><p>So the right question isn&#8217;t &#8220;can I use recursion here?&#8221; but rather &#8220;is recursion easier here?&#8221; There are a few questions you can ask yourself to figure this out.</p><ol><li><p><strong>Does the problem break down into subproblems? </strong>This is mostly what recursion is about. If a problem breaks into smaller versions of itself, recursion is probably a good fit. For example, sorting an array can be decomposed into sorting two halves and merging them. Finding a path through a maze can be broken into taking one step and finding a path from there.</p></li><li><p><strong>Could you solve the problem using an arbitrary number of for loops? </strong>This point gets people confused. Let&#8217;s say you are asked to print all possible 6-digit numbers. It sounds easy: you can just write six nested for loops, one for each digit. But what if you are told to print all possible n-digit numbers? You can't write n nested for loops anymore since the number of loops changes depending on the input. This is an excellent example of when to use recursion, because it effectively allows you to create arbitrary nesting.</p></li><li><p><strong>Does the problem fit one of the common recursive patterns? </strong>For most recursive problems you see in interviews, there are six patterns that you can look for. We&#8217;ll preview them at the end of this article and cover them in depth in future ones. If you recognize that a problem fits one of these patterns, that is a strong hint to use recursion.</p></li><li><p><strong>Is it easier to solve recursively than iteratively? </strong>Sometimes the recursive code is just cleaner. Consider the task of printing a linked list in reverse order. Without recursion, you would have to push all the values onto a stack, then pop and print them. Recursively, the process reduces to three lines of code:</p></li></ol><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9f4d3456-0fdc-4f38-94e8-8cac1b7e8a5c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def print_reverse(node):

  if node is None:
    return

  print_reverse(node.next)
  print(node.value)</code></pre></div><p>The recursive version uses the call stack as your stack, so you don&#8217;t need to explicitly manage one yourself. The code is more concise and probably easier to grasp.</p><p>However, there is a trade-off. If the time complexity is the same, the iterative solution usually runs faster. Function calls, stack frames, and other things that recursion does all add up. In general, if you have the possibility to choose between two equally simple recursive and iterative solutions, the iterative one will perform better. </p><p>But in an interview, clarity often matters more than micro-optimizations, so it is wise to pick whatever approach lets you write correct, readable code faster.</p><h2>Base Case and Recursive Step</h2><p>Every recursive function has two essential building blocks: a base case and a recursive step. Getting these right is what separates working code from infinite loops and stack overflows.</p><p>The base case is the point at which the recursion stops. It is the simplest form of the problem, and it can be solved immediately without any further recursive calls.</p><p>A useful technique for identifying the base case is to start with small examples. What is the answer when the input size is zero or one? These edge cases are usually the base cases.</p><p>For example, if you are writing a function to compute the sum of an array of integers, an empty array has a sum of 0, and an array with one element has a sum equal to that element. Both can be solved without recursion, making them natural base cases.</p><p>There is one important rule to follow here: the base case must match the return type of your function. If your function returns a list, your base case must return a list (not None or <code>0</code>). This sounds obvious, but it is a common source of bugs. People often get stuck on this because they try to guess what the base case is instead of using concrete examples to figure it out.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3880c540-d3df-49f9-8865-254ac40bd018&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Wrong: function returns a list, but base case returns None
def get_combinations(items):
    if len(items) == 0:
        return None  # This will break the calling code
    ...

# Correct: base case returns an empty list
def get_combinations(items):
    if len(items) == 0:
        return [[]]  # A list containing the empty combination
    ...</code></pre></div><p>In the recursive step,  you divide the problem into smaller subproblems and combine the results. There are two things to keep in mind here.</p><p>The first is that a subproblem should have the same shape as the original problem. If your function computes the sum of elements of an array from index i to the end, then your recursive call should compute the sum of elements from index i+1 to the end. The meaning is the same, just applied to a smaller input.</p><p>The second critical thing is that the recursive step must converge to the base case. If your base case is i == 0 and your recursive call passes i + 1, you are moving away from the base case, not toward it. This causes an infinite recursion and eventually a stack overflow.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8d8de827-f213-4e0a-82a7-fe79f180cff1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Wrong: moves away from base case
def countdown(n):
    if n == 0:
        return
    countdown(n + 1)  # n grows, never reaches 0

# Correct: moves toward base case
def countdown(n):
    if n == 0:
        return
    countdown(n - 1)  # n shrinks toward 0</code></pre></div><p>When you are writing code in an interview, you don&#8217;t get to run your code and see the stack overflow error. You need to find this mistake by looking at the code you wrote. A quick check that your recursive step gets you closer to the base case can save you from an embarrassing bug.</p><h2>Three Ways to Return Results</h2><p>When you write a recursive function, you need to decide how to collect and return the results. There are three main ways you can do this: use a global variable, use a passed variable, or build up results as you return. Each has its trade-offs.</p><p>The simplest approach is to use a global variable. You set a variable outside the code and change it as the recursion proceeds. As an example, consider the following code snippet that counts all the even numbers in an array:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1ea670e3-d346-4aa4-8a4b-d4d9abf59e02&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def count_evens(arr, i):
    global result
    if i == len(arr):
        return
    if arr[i] % 2 == 0:
        result += 1
    count_evens(arr, i + 1)</code></pre></div><p>This works, but global variables are generally not a good idea. They make code harder to test and reason about. A better alternative is to use a passed variable.</p><p>When using a passed variable, you pass a mutable object (like a list or a wrapper class) into your function and update it as you go through the recursive calls. This stays away from a global state and follows simple logic:</p>
      <p>
          <a href="https://newsletter.francofernando.com/p/mastering-recursion-the-foundations">
              Read more
          </a>
      </p>
   ]]></content:encoded></item></channel></rss>