<?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[Finn Christiansen]]></title><description><![CDATA[Finn Christiansen]]></description><link>https://blog.finnchristiansen.com</link><image><url>https://blog.finnchristiansen.com/img/substack.png</url><title>Finn Christiansen</title><link>https://blog.finnchristiansen.com</link></image><generator>Substack</generator><lastBuildDate>Wed, 06 May 2026 11:18:43 GMT</lastBuildDate><atom:link href="https://blog.finnchristiansen.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Finn Christiansen]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[finnchristiansen@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[finnchristiansen@substack.com]]></itunes:email><itunes:name><![CDATA[Finn Christiansen]]></itunes:name></itunes:owner><itunes:author><![CDATA[Finn Christiansen]]></itunes:author><googleplay:owner><![CDATA[finnchristiansen@substack.com]]></googleplay:owner><googleplay:email><![CDATA[finnchristiansen@substack.com]]></googleplay:email><googleplay:author><![CDATA[Finn Christiansen]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[You’re Not Clumsy—You’re Just Mismanaging Your Error Budget]]></title><description><![CDATA[From dropped cups to software outages: rethink your margin for mistakes.]]></description><link>https://blog.finnchristiansen.com/p/youre-not-clumsyyoure-just-mismanaging</link><guid isPermaLink="false">https://blog.finnchristiansen.com/p/youre-not-clumsyyoure-just-mismanaging</guid><dc:creator><![CDATA[Finn Christiansen]]></dc:creator><pubDate>Thu, 24 Jul 2025 14:03:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!8gbm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F489a3b40-626a-40d8-af84-df214ff8efcf_364x471.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_!8gbm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F489a3b40-626a-40d8-af84-df214ff8efcf_364x471.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8gbm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F489a3b40-626a-40d8-af84-df214ff8efcf_364x471.png 424w, https://substackcdn.com/image/fetch/$s_!8gbm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F489a3b40-626a-40d8-af84-df214ff8efcf_364x471.png 848w, https://substackcdn.com/image/fetch/$s_!8gbm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F489a3b40-626a-40d8-af84-df214ff8efcf_364x471.png 1272w, https://substackcdn.com/image/fetch/$s_!8gbm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F489a3b40-626a-40d8-af84-df214ff8efcf_364x471.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8gbm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F489a3b40-626a-40d8-af84-df214ff8efcf_364x471.png" width="364" height="471" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/489a3b40-626a-40d8-af84-df214ff8efcf_364x471.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:471,&quot;width&quot;:364,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:273541,&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://blog.finnchristiansen.com/i/169112801?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F489a3b40-626a-40d8-af84-df214ff8efcf_364x471.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_!8gbm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F489a3b40-626a-40d8-af84-df214ff8efcf_364x471.png 424w, https://substackcdn.com/image/fetch/$s_!8gbm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F489a3b40-626a-40d8-af84-df214ff8efcf_364x471.png 848w, https://substackcdn.com/image/fetch/$s_!8gbm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F489a3b40-626a-40d8-af84-df214ff8efcf_364x471.png 1272w, https://substackcdn.com/image/fetch/$s_!8gbm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F489a3b40-626a-40d8-af84-df214ff8efcf_364x471.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 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></p><p>When people talk about <em>error culture</em>, there&#8217;s a common story that pops up:</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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><p>You drop a cup in the kitchen as a kid.</p><p>Your parent yells: &#8220;Why can&#8217;t you pay attention just <em>once</em>?&#8221;</p><p>It&#8217;s a familiar moment for many&#8212;and also a classic fallacy.</p><p>Here&#8217;s the problem: the reaction assumes attention is binary. Either you&#8217;re paying 100% attention and nothing ever breaks, or you&#8217;re somewhere between 0&#8211;99%, and eventually something <em>will</em> break.</p><p>So if you want to &#8220;win,&#8221; you have to pay <em>perfect</em> attention <em>all the time</em>. That might sound responsible&#8212;but it&#8217;s incredibly inefficient.</p><p>Let&#8217;s talk opportunity cost.</p><p>In the example with a child, the cost of constantly paying attention isn&#8217;t obvious. Some might argue, &#8220;What&#8217;s the problem? The kid has plenty of time and energy. But we&#8212;the adults&#8212;have limited time and budget.&#8221;</p><p>Fair enough. But let&#8217;s take a different example where the cost is more tangible:</p><p>I&#8217;m self-employed and work full-time. My partner does too. We try to stay healthy by cooking meals from scratch, one or two times a day.</p><p>That&#8217;s 10 meals a week. At roughly 30 minutes per meal, that&#8217;s 5 hours. Based on my hourly rate, those hours have a real monetary cost&#8212;well into the three figures.</p><p>Meanwhile, our kitchen gear? Mostly Ikea-level basics. For example, the <em>Pokal</em> glass from Ikea costs &#8364;0.59.</p><p>I break something maybe once every one to two months.</p><p>You don&#8217;t need a PhD in math to realize: spending extra time and energy being ultra-careful with &#8364;0.59 items simply doesn&#8217;t make economic sense. The opportunity cost of working slowly far outweighs the occasional broken glass.</p><p>So for me, the only logical conclusion: <strong>I&#8217;m working too slowly in the kitchen.</strong></p><p>This realization only clicked for me after reading about the concept of an <strong>error budget</strong> in the book <em>Site Reliability Engineering: How Google Runs Production Systems</em>.</p><p>The idea is simple:</p><ul><li><p>Define an acceptable rate of failure&#8212;an error budget.</p></li><li><p>If you&#8217;re within budget: go faster, take reasonable risks.</p></li><li><p>If you&#8217;re over budget: slow down, invest in stability.</p></li></ul><p>An error budget gives your team something critical: <strong>a foundation for discussion</strong> and <strong>a guiding star</strong> for the messy reality of day-to-day operations.</p><p>It helps avoid the trap of striving for perfection when it&#8217;s not economically justified. It replaces shame with pragmatism.</p><p>So let me ask you:</p><p><strong>What&#8217;s your error budget in the kitchen? How many glasses per month is it okay to break?</strong></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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[I have this new approach. But it only works in perfection…]]></title><description><![CDATA[Organizational change should be a dial&#8212;not a switch.]]></description><link>https://blog.finnchristiansen.com/p/i-have-this-new-approach-but-it-only</link><guid isPermaLink="false">https://blog.finnchristiansen.com/p/i-have-this-new-approach-but-it-only</guid><dc:creator><![CDATA[Finn Christiansen]]></dc:creator><pubDate>Wed, 23 Jul 2025 14:03:34 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!gIcm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F61f1f86b-74b0-4ff3-aca2-15d7a490f096_761x480.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_!gIcm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F61f1f86b-74b0-4ff3-aca2-15d7a490f096_761x480.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!gIcm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F61f1f86b-74b0-4ff3-aca2-15d7a490f096_761x480.png 424w, https://substackcdn.com/image/fetch/$s_!gIcm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F61f1f86b-74b0-4ff3-aca2-15d7a490f096_761x480.png 848w, https://substackcdn.com/image/fetch/$s_!gIcm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F61f1f86b-74b0-4ff3-aca2-15d7a490f096_761x480.png 1272w, https://substackcdn.com/image/fetch/$s_!gIcm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F61f1f86b-74b0-4ff3-aca2-15d7a490f096_761x480.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!gIcm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F61f1f86b-74b0-4ff3-aca2-15d7a490f096_761x480.png" width="761" height="480" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/61f1f86b-74b0-4ff3-aca2-15d7a490f096_761x480.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:480,&quot;width&quot;:761,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:130434,&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://blog.finnchristiansen.com/i/169023142?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F61f1f86b-74b0-4ff3-aca2-15d7a490f096_761x480.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_!gIcm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F61f1f86b-74b0-4ff3-aca2-15d7a490f096_761x480.png 424w, https://substackcdn.com/image/fetch/$s_!gIcm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F61f1f86b-74b0-4ff3-aca2-15d7a490f096_761x480.png 848w, https://substackcdn.com/image/fetch/$s_!gIcm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F61f1f86b-74b0-4ff3-aca2-15d7a490f096_761x480.png 1272w, https://substackcdn.com/image/fetch/$s_!gIcm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F61f1f86b-74b0-4ff3-aca2-15d7a490f096_761x480.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 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>I&#8217;ve been experimenting with a new approach lately. It&#8217;s elegant, modern, and ambitious. But it only really works&#8230; if everything goes perfectly.</p><p>You&#8217;ve probably seen similar patterns with the latest &#8220;real&#8221; continuous delivery setups:</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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><ul><li><p>You work in pairs and perform continuous code reviews.</p></li><li><p>You commit directly to the main branch.</p></li><li><p>Your changes are tested&#8212;but only your own microservice. God forbid that you test it together with other services.</p></li><li><p>The changes are deployed directly to production. Of course, everything is covered by perfect and all-encompassing integration and end-to-end tests.</p></li><li><p>If something fails, the changes are automatically reversed.</p></li><li><p>Naturally, all your database migrations are designed to be fully backwards-compatible.</p></li></ul><p>Sounds like a great plan&#8212;<strong>if all the planets are aligned</strong>.</p><p>But let&#8217;s be honest: how often <em>are</em> the planets aligned?</p><p>My impression is that many of these approaches share a common characteristic: the relationship between how well you implement the process and the benefit you get isn&#8217;t continuous&#8212;or even linear. It&#8217;s more like a discontinuous jump function. You&#8217;re either doing it perfectly or you&#8217;re in trouble.</p><p>I&#8217;ve seen this dynamic before, often involving coaches or consultants:</p><ul><li><p>You pay a hefty price.</p></li><li><p>You allocate significant capacity across your team.</p></li><li><p>You&#8217;re proud of reaching 90% adoption of the new model.</p></li><li><p>Then something breaks. Everything falls apart.</p></li><li><p>And the coach says: &#8220;Well, it&#8217;s not working because you missed this minor detail.&#8221;</p></li></ul><p>They often come armed with a collection of wise-sounding quotes:</p><p>&#8220;I can show you the door. I can even open it for you. But you have to go through it yourself.&#8221;</p><p>&#8220;Sometimes it must get worse before it gets better.&#8221;</p><p>&#8220;Maybe the downtime of your production system isn&#8217;t a technical problem. Maybe it&#8217;s a symptom of your mindset."</p><p>Sure, some systems in life are truly binary. You&#8217;re either pregnant or you&#8217;re not. There&#8217;s no such thing as being <em>a little bit pregnant</em>.</p><p>But most organizational changes should yield <strong>continuous output</strong> based on <strong>continuous input</strong>. You put in 20% effort, you get a return. You put in 50%, and you get even more return. That&#8217;s how most real-world systems&#8212;and the <strong>Pareto Principle</strong>&#8212;work.</p><p>So if a new approach requires <em>complete</em> adherence before it delivers <em>any</em> results, it&#8217;s worth pausing.</p><p>Because then, you&#8217;re no longer optimizing. You&#8217;re gambling.</p><p>And if it really <em>is</em> a binary system, then the risk-reward ratio must be evaluated carefully. Otherwise, you might find yourself in a situation where you&#8217;ve invested everything and get nothing&#8212;because one minor clause wasn&#8217;t fulfilled.</p><p>What&#8217;s your take on approaches that only work when done 100% right?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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[Kafka Without the Complexity? That’s what Redpanda promises… ]]></title><description><![CDATA[Can Redpanda finally make event streaming accessible to everyone?]]></description><link>https://blog.finnchristiansen.com/p/kafka-without-the-complexity-thats</link><guid isPermaLink="false">https://blog.finnchristiansen.com/p/kafka-without-the-complexity-thats</guid><dc:creator><![CDATA[Finn Christiansen]]></dc:creator><pubDate>Tue, 22 Jul 2025 14:02:53 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!JPXa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce453c98-4ce6-4a81-82cd-d766d4987212_475x309.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_!JPXa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce453c98-4ce6-4a81-82cd-d766d4987212_475x309.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JPXa!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce453c98-4ce6-4a81-82cd-d766d4987212_475x309.png 424w, https://substackcdn.com/image/fetch/$s_!JPXa!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce453c98-4ce6-4a81-82cd-d766d4987212_475x309.png 848w, https://substackcdn.com/image/fetch/$s_!JPXa!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce453c98-4ce6-4a81-82cd-d766d4987212_475x309.png 1272w, https://substackcdn.com/image/fetch/$s_!JPXa!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce453c98-4ce6-4a81-82cd-d766d4987212_475x309.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JPXa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce453c98-4ce6-4a81-82cd-d766d4987212_475x309.png" width="475" height="309" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ce453c98-4ce6-4a81-82cd-d766d4987212_475x309.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:309,&quot;width&quot;:475,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:35203,&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://blog.finnchristiansen.com/i/168925146?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce453c98-4ce6-4a81-82cd-d766d4987212_475x309.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_!JPXa!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce453c98-4ce6-4a81-82cd-d766d4987212_475x309.png 424w, https://substackcdn.com/image/fetch/$s_!JPXa!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce453c98-4ce6-4a81-82cd-d766d4987212_475x309.png 848w, https://substackcdn.com/image/fetch/$s_!JPXa!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce453c98-4ce6-4a81-82cd-d766d4987212_475x309.png 1272w, https://substackcdn.com/image/fetch/$s_!JPXa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce453c98-4ce6-4a81-82cd-d766d4987212_475x309.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 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>I recently stumbled across Redpanda, and I have to say&#8212;it looks like a really promising alternative to Apache Kafka.</p><p>On paper, Redpanda offers some compelling benefits:</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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><ul><li><p>Kafka compatible</p></li><li><p>Simpler to run: Just a single C++ binary. No JVM. No ZooKeeper. No extra services.</p></li><li><p>AI-first and built for global scale</p></li></ul><p>The pitch seems tailored for massive-scale applications&#8212;but what caught my attention is how much it might benefit local development and smaller projects too.</p><p>Kafka has long been the standard, but let&#8217;s be honest: running Kafka locally is a pain. You need multiple services, ZooKeeper, a bunch of tuning, and still end up wrestling with configs just to get things working.</p><p>Redpanda promises to flip that experience.</p><p>With Docker Compose, you can spin up a single Redpanda broker. Suddenly, the barrier to using an event streaming platform locally becomes much lower. For prototyping or small team environments, this kind of simplicity could be a game-changer.</p><p>Now, let&#8217;s talk pricing.</p><p>Redpanda Serverless in AWS&#8217;s eu-central-1 region starts at $171.74/month (about &#8364;147.60), offering a write throughput of 0.1 MB/s. By comparison, a managed Kafka cluster in Frankfurt clocks in at around $148.80 (&#8364;127.31).</p><p>Question to the &#8220;Event Driven Bros&#8221; here: Is Redpanda an alternative for smaller projects?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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[Optimizing throughput sounds smart—until it costs you real money.]]></title><description><![CDATA[How consumer habits sneak into business decisions&#8212;and hurt performance.]]></description><link>https://blog.finnchristiansen.com/p/optimizing-throughput-sounds-smartuntil</link><guid isPermaLink="false">https://blog.finnchristiansen.com/p/optimizing-throughput-sounds-smartuntil</guid><dc:creator><![CDATA[Finn Christiansen]]></dc:creator><pubDate>Sun, 20 Jul 2025 05:55:12 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Gd9Z!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a795b2-10b2-4bbb-8ba7-27a6056440d5_500x500.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_!Gd9Z!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a795b2-10b2-4bbb-8ba7-27a6056440d5_500x500.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Gd9Z!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a795b2-10b2-4bbb-8ba7-27a6056440d5_500x500.png 424w, https://substackcdn.com/image/fetch/$s_!Gd9Z!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a795b2-10b2-4bbb-8ba7-27a6056440d5_500x500.png 848w, https://substackcdn.com/image/fetch/$s_!Gd9Z!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a795b2-10b2-4bbb-8ba7-27a6056440d5_500x500.png 1272w, https://substackcdn.com/image/fetch/$s_!Gd9Z!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a795b2-10b2-4bbb-8ba7-27a6056440d5_500x500.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Gd9Z!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a795b2-10b2-4bbb-8ba7-27a6056440d5_500x500.png" width="500" height="500" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/94a795b2-10b2-4bbb-8ba7-27a6056440d5_500x500.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:500,&quot;width&quot;:500,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:230754,&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://blog.finnchristiansen.com/i/168760576?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a795b2-10b2-4bbb-8ba7-27a6056440d5_500x500.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_!Gd9Z!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a795b2-10b2-4bbb-8ba7-27a6056440d5_500x500.png 424w, https://substackcdn.com/image/fetch/$s_!Gd9Z!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a795b2-10b2-4bbb-8ba7-27a6056440d5_500x500.png 848w, https://substackcdn.com/image/fetch/$s_!Gd9Z!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a795b2-10b2-4bbb-8ba7-27a6056440d5_500x500.png 1272w, https://substackcdn.com/image/fetch/$s_!Gd9Z!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a795b2-10b2-4bbb-8ba7-27a6056440d5_500x500.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 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>Recently, I had what might be the worst travel experience of my life. A combination of the worst airport and the worst airline.<br><br>First, we stood in line at the gate&#8212;standard fare. Then we were herded down a narrow staircase into a holding room. After that, they led us into a fenced-off corridor, standing outside without any shelter, in the cold, without jackets, like cattle on their final walk to the slaughterhouse.<br><br>Why? All so that the plane wouldn&#8217;t spend a single unnecessary second on the ground.<br><br>This kind of "efficiency" isn't unique to airlines. You see it everywhere in B2C&#8212;where most of us spend our lives as consumers, especially in our early years.<br><br>It's always about maximizing throughput. The actual lead time&#8212;how long it takes from intent to outcome&#8212;is often ignored or forgotten altogether.<br><br>We&#8217;re conditioned for this from a young age.<br><br>Remember being told off for starting the dishwasher when &#120363;&#120374;&#120372;&#120373; &#120368;&#120367;&#120358; &#120366;&#120368;&#120371;&#120358; &#120369;&#120365;&#120354;&#120373;&#120358; could&#8217;ve fit? But what if there &#120376;&#120354;&#120372;&#120367;&#8217;&#120373; another dirty plate at the time? So you wait. After the next meal, you add the plate&#8212;and now the kitchen is full of dirty dishes. Great throughput, terrible lead time.<br><br>And this mindset creeps into our business decisions.<br><br>Take the Vercel and Next.js community, for example. You often see posts like:<br><br>"I&#8217;ve built this awesome SaaS app, but nobody&#8217;s using it yet. When someone &#120357;&#120368;&#120358;&#120372; visit, it takes 4 seconds to load. What can I do?"<br><br>Here&#8217;s an idea: put it in a container and keep your cache warm.<br><br>"But then I&#8217;m paying for a container that nobody uses! That&#8217;s wasteful!"<br><br>That right there is the flaw in thinking.<br><br>Sure, your app isn't used most of the time&#8212;but when someone &#120357;&#120368;&#120358;&#120372; show up, you have seconds to make a good impression. Those cold-start delays can kill a lead.<br><br>Let&#8217;s do the math: If you need 10 website visits to get 1 click to your web app, and 10 users in your app to get 1 sign-up, that&#8217;s 100 ad clicks per registration. At &#8364;5 per click in a B2B market, that&#8217;s &#8364;500 for a single registration.<br><br>And you're worried about &#8364;10/month for a warm container?<br><br>You're not going to get that kind of cost-efficiency on AWS when you factor in hidden costs like load balancers. But platforms like DigitalOcean's App Platform make this kind of setup easily achievable.<br><br><br>So here&#8217;s my question to you:<br>When you run the dishwasher even though it &#120356;&#120368;&#120374;&#120365;&#120357; have fit one more plate&#8212;are you being wasteful, or are you optimizing for lead time?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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[Serverless Done Right: Why Hono Beats Express and Nest for Me]]></title><description><![CDATA[Why this lesser-known Node.js framework deserves your attention.]]></description><link>https://blog.finnchristiansen.com/p/serverless-done-right-why-hono-beats</link><guid isPermaLink="false">https://blog.finnchristiansen.com/p/serverless-done-right-why-hono-beats</guid><dc:creator><![CDATA[Finn Christiansen]]></dc:creator><pubDate>Fri, 18 Jul 2025 14:02:53 GMT</pubDate><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!q944!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3dbb719-0550-45d7-ae97-b53b570c7227_493x237.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!q944!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3dbb719-0550-45d7-ae97-b53b570c7227_493x237.png 424w, https://substackcdn.com/image/fetch/$s_!q944!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3dbb719-0550-45d7-ae97-b53b570c7227_493x237.png 848w, https://substackcdn.com/image/fetch/$s_!q944!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3dbb719-0550-45d7-ae97-b53b570c7227_493x237.png 1272w, https://substackcdn.com/image/fetch/$s_!q944!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3dbb719-0550-45d7-ae97-b53b570c7227_493x237.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!q944!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3dbb719-0550-45d7-ae97-b53b570c7227_493x237.png" width="493" height="237" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b3dbb719-0550-45d7-ae97-b53b570c7227_493x237.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:237,&quot;width&quot;:493,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:23342,&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://blog.finnchristiansen.com/i/168619184?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3dbb719-0550-45d7-ae97-b53b570c7227_493x237.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_!q944!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3dbb719-0550-45d7-ae97-b53b570c7227_493x237.png 424w, https://substackcdn.com/image/fetch/$s_!q944!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3dbb719-0550-45d7-ae97-b53b570c7227_493x237.png 848w, https://substackcdn.com/image/fetch/$s_!q944!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3dbb719-0550-45d7-ae97-b53b570c7227_493x237.png 1272w, https://substackcdn.com/image/fetch/$s_!q944!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3dbb719-0550-45d7-ae97-b53b570c7227_493x237.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>There are several approaches when running Node.js applications: libraries like Express.js and frameworks like Nest.js. If you're using serverless functions like AWS Lambda, there's even the option to implement handler functions from scratch&#8212;sometimes with the help of smaller libraries like Middy for middleware management.</p><p>However, there's a catch when working with Lambda functions:</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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><p>It's highly beneficial to run your backend&#8212;comprising multiple Lambda functions&#8212;locally during development.</p><p>There are a few ways to achieve this: using emulators like LocalStack, wrappers like <code>serverless-http</code> for Express.js, or writing a custom wrapper&#8212;such as an Express.js app that calls your plain Lambda handlers in its routes.</p><p>That doesn&#8217;t feel very native and sounds a bit hacky, right?</p><p>That&#8217;s why my favorite lightweight framework for serverless applications is <strong>Hono</strong>!</p><p>Hono sits in the middle of the spectrum&#8212;from minimal libraries like Middy to full-blown frameworks like Nest.js. It&#8217;s more than a simple routing library like Express.js, yet doesn&#8217;t come with the steep learning curve and heavy abstractions of Nest.js.</p><p>For example, you can throw an <code>HTTPException</code> with a status code and message anywhere in your code, and Hono will automatically generate the appropriate HTTP response.</p><p>Hono also introduces the concept of an app that is independent of the runtime&#8212;whether it's AWS Lambda or containerized Node.js. Your app defines routes, middleware, and logic, and you simply specify the runtime. Hono handles the rest, enabling you to run the exact same backend both in AWS Lambda and locally as a standard Node.js application.</p><p>Initially, I was hesitant when someone introduced me to Hono. Back then&#8212;two years ago&#8212;it was relatively new and hadn&#8217;t gained much traction on GitHub.</p><p>But that has changed: as of July 2025, Hono has 25,000 stars on GitHub. For comparison, Express.js has 64,000 and Nest.js has 71,000 stars.</p><p>Given how much younger Hono is, I believe it has a very promising future.</p><p>One question I often hear is:</p><p><strong>&#8220;Cool new tech&#8212;but have you actually used it in production within an enterprise context? Or is your experience just from weekend side projects with zero users?&#8221;</strong></p><p>I can confidently say that I&#8217;ve used Hono in production in an enterprise setting&#8212;and I&#8217;ve been very impressed. It&#8217;s now my go-to framework for backends, both in serverless and containerized Node.js environments.</p><p><strong>What&#8217;s your favorite framework?</strong></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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[From PostgreSQL to ElasticSearch: The Power of CQRS in Real Projects]]></title><description><![CDATA[Why separating reads and writes can lead to simpler, faster systems]]></description><link>https://blog.finnchristiansen.com/p/from-postgresql-to-elasticsearch</link><guid isPermaLink="false">https://blog.finnchristiansen.com/p/from-postgresql-to-elasticsearch</guid><dc:creator><![CDATA[Finn Christiansen]]></dc:creator><pubDate>Wed, 16 Jul 2025 14:01:49 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!e0yZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F913bc94c-2211-4e92-b716-f33ffbc1a3d0_724x662.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_!e0yZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F913bc94c-2211-4e92-b716-f33ffbc1a3d0_724x662.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!e0yZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F913bc94c-2211-4e92-b716-f33ffbc1a3d0_724x662.png 424w, https://substackcdn.com/image/fetch/$s_!e0yZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F913bc94c-2211-4e92-b716-f33ffbc1a3d0_724x662.png 848w, https://substackcdn.com/image/fetch/$s_!e0yZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F913bc94c-2211-4e92-b716-f33ffbc1a3d0_724x662.png 1272w, https://substackcdn.com/image/fetch/$s_!e0yZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F913bc94c-2211-4e92-b716-f33ffbc1a3d0_724x662.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!e0yZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F913bc94c-2211-4e92-b716-f33ffbc1a3d0_724x662.png" width="724" height="662" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/913bc94c-2211-4e92-b716-f33ffbc1a3d0_724x662.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:662,&quot;width&quot;:724,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:64523,&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://blog.finnchristiansen.com/i/168448069?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F913bc94c-2211-4e92-b716-f33ffbc1a3d0_724x662.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_!e0yZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F913bc94c-2211-4e92-b716-f33ffbc1a3d0_724x662.png 424w, https://substackcdn.com/image/fetch/$s_!e0yZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F913bc94c-2211-4e92-b716-f33ffbc1a3d0_724x662.png 848w, https://substackcdn.com/image/fetch/$s_!e0yZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F913bc94c-2211-4e92-b716-f33ffbc1a3d0_724x662.png 1272w, https://substackcdn.com/image/fetch/$s_!e0yZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F913bc94c-2211-4e92-b716-f33ffbc1a3d0_724x662.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 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>I recently watched <a href="https://www.youtube.com/watch?v=hP-2ojGfd-Q">some videos</a> by Golo Roden from<a href="https://www.youtube.com/@thenativeweb"> </a><em><a href="https://www.youtube.com/@thenativeweb">the native web GmbH</a></em> on his YouTube channel, and they sparked some thoughts around CQRS and Event Sourcing.</p><p>Let&#8217;s take a practical example: implementing a marketplace where users can search for bicycles.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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><p>In such a system, you usually have two very different concerns&#8212;writing data (like creating or updating listings) and reading data (like searching for bikes based on filters). That&#8217;s where the CQRS (Command Query Responsibility Segregation) pattern comes into play.</p><p>The idea is simple but powerful: split your application into two parts. The <em>write</em> side handles commands&#8212;data changes&#8212;and writes to a relational database like PostgreSQL. PostgreSQL offers strong consistency, referential integrity, and powerful features like constraints and transactions. It&#8217;s the ideal choice for managing critical business data with precision.</p><p>On the <em>read</em> side, you want to provide a smooth and fast experience when users are searching for bikes. This is where ElasticSearch enters the picture.</p><p>The data is denormalized and synced from PostgreSQL into ElasticSearch. Denormalized means it's tailored for fast access: no complex joins, no foreign key lookups, just flat and queryable data.</p><p>The biggest advantage? Your queries become much simpler. You get automatic indexing, which is a huge time saver. Need full-text search across multiple fields? Done. Want to aggregate results by location, brand, or price range? ElasticSearch handles that effortlessly. These are things that would require non-trivial SQL and manual index tuning in PostgreSQL.</p><p>So with CQRS and ElasticSearch, you decouple complex domain logic from high-performance querying. You get strong guarantees on the write side and fast, flexible queries on the read side.</p><p>And now you: Have you used CQRS with ElasticSearch in your projects?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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[How Ephemeral Environments Prevent Cloud Breakages]]></title><description><![CDATA[Why local testing isn&#8217;t enough in a serverless world]]></description><link>https://blog.finnchristiansen.com/p/how-ephemeral-environments-prevent</link><guid isPermaLink="false">https://blog.finnchristiansen.com/p/how-ephemeral-environments-prevent</guid><dc:creator><![CDATA[Finn Christiansen]]></dc:creator><pubDate>Tue, 15 Jul 2025 17:54:58 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!s-7s!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdabb7572-a583-4942-b07a-6f5b29407601_344x522.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_!s-7s!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdabb7572-a583-4942-b07a-6f5b29407601_344x522.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!s-7s!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdabb7572-a583-4942-b07a-6f5b29407601_344x522.png 424w, https://substackcdn.com/image/fetch/$s_!s-7s!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdabb7572-a583-4942-b07a-6f5b29407601_344x522.png 848w, https://substackcdn.com/image/fetch/$s_!s-7s!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdabb7572-a583-4942-b07a-6f5b29407601_344x522.png 1272w, https://substackcdn.com/image/fetch/$s_!s-7s!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdabb7572-a583-4942-b07a-6f5b29407601_344x522.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!s-7s!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdabb7572-a583-4942-b07a-6f5b29407601_344x522.png" width="344" height="522" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dabb7572-a583-4942-b07a-6f5b29407601_344x522.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:522,&quot;width&quot;:344,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:38377,&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://blog.finnchristiansen.com/i/168407009?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdabb7572-a583-4942-b07a-6f5b29407601_344x522.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_!s-7s!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdabb7572-a583-4942-b07a-6f5b29407601_344x522.png 424w, https://substackcdn.com/image/fetch/$s_!s-7s!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdabb7572-a583-4942-b07a-6f5b29407601_344x522.png 848w, https://substackcdn.com/image/fetch/$s_!s-7s!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdabb7572-a583-4942-b07a-6f5b29407601_344x522.png 1272w, https://substackcdn.com/image/fetch/$s_!s-7s!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdabb7572-a583-4942-b07a-6f5b29407601_344x522.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 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>If you're developing serverless applications in the cloud, you're likely familiar with this scenario: You create a pull request (PR) with your changes to introduce a new feature. After all the static code analysis and unit tests pass successfully, the PR gets reviewed and merged.</p><p>But then&#8212;what&#8217;s that?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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><p>After merging the changes, the CI pipeline deploys them&#8230; and everything breaks!</p><p><strong>What&#8217;s going on?</strong></p><p>It comes down to distinguishing between build-time and run-time errors. In the old days of on-premise infrastructure, you could be reasonably confident that you'd catch run-time errors while testing locally.</p><p>A typical setup might include a Java backend and a PostgreSQL database. If calling an endpoint that runs a database query worked locally, it was unlikely to fail at runtime in production.</p><p>But in a typical serverless environment, things work differently:</p><p>Let&#8217;s say you have an API Gateway that exposes a REST interface and invokes a TypeScript Lambda function, which stores data in a DynamoDB table.</p><p>While developing locally, you might:</p><ul><li><p>Mock the AWS-specific dependencies</p></li><li><p>Use emulators like LocalStack</p></li><li><p>Abstract the cloud calls and replace them with local alternatives (e.g., writing to disk instead of uploading to S3)</p></li></ul><p><strong>But this is exactly the problem!</strong></p><p>What are you really testing? You're only verifying that your application works with your mocked, emulated, or abstracted dependencies.</p><p><strong>The solution? Ephemeral Environments.</strong><br>For every PR, provision all the required cloud resources and deploy the PR&#8217;s code into that environment. If needed, you can copy production data or seed services like S3 and DynamoDB with test data.</p><p>This setup enables you to run integration and end-to-end tests <em>before</em> merging changes into the main branch.</p><p>Some might suggest another approach: run the Lambda handler locally while using real AWS resources like DynamoDB. But this introduces risk&#8212;using the same resources as your production environment is dangerous.</p><p><strong>And that&#8217;s the key:</strong><br>You need dedicated, isolated resources for each PR.</p><p>Which brings us back to ephemeral environments.</p><p><strong>Now it's your turn: Are you using ephemeral environments?</strong></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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[Intuition vs. Reality: Understanding Cloud Cost Traps]]></title><description><![CDATA[A real-world example of why cloud pricing can be unintuitive]]></description><link>https://blog.finnchristiansen.com/p/intuition-vs-reality-understanding</link><guid isPermaLink="false">https://blog.finnchristiansen.com/p/intuition-vs-reality-understanding</guid><dc:creator><![CDATA[Finn Christiansen]]></dc:creator><pubDate>Mon, 14 Jul 2025 14:02:54 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!QyH9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2698664-2677-44af-a0c9-9c5cce51ad67_500x500.jpeg" 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_!QyH9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2698664-2677-44af-a0c9-9c5cce51ad67_500x500.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QyH9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2698664-2677-44af-a0c9-9c5cce51ad67_500x500.jpeg 424w, https://substackcdn.com/image/fetch/$s_!QyH9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2698664-2677-44af-a0c9-9c5cce51ad67_500x500.jpeg 848w, https://substackcdn.com/image/fetch/$s_!QyH9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2698664-2677-44af-a0c9-9c5cce51ad67_500x500.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!QyH9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2698664-2677-44af-a0c9-9c5cce51ad67_500x500.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QyH9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2698664-2677-44af-a0c9-9c5cce51ad67_500x500.jpeg" width="500" height="500" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a2698664-2677-44af-a0c9-9c5cce51ad67_500x500.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:500,&quot;width&quot;:500,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:49293,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.finnchristiansen.com/i/168267684?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2698664-2677-44af-a0c9-9c5cce51ad67_500x500.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!QyH9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2698664-2677-44af-a0c9-9c5cce51ad67_500x500.jpeg 424w, https://substackcdn.com/image/fetch/$s_!QyH9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2698664-2677-44af-a0c9-9c5cce51ad67_500x500.jpeg 848w, https://substackcdn.com/image/fetch/$s_!QyH9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2698664-2677-44af-a0c9-9c5cce51ad67_500x500.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!QyH9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2698664-2677-44af-a0c9-9c5cce51ad67_500x500.jpeg 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 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>As software developers, we often have an intuitive understanding of infrastructure costs and the expenses associated with certain backend operations.</p><p>Right?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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><p>That may have been true in the pre-public cloud era and is still partially true for hyperscalers in the public cloud, such as AWS.</p><p>However, the problem is that many cloud resources are not billed &#8220;per item&#8221; (e.g., &#8220;per DynamoDB table&#8221;) but rather per API operation. And here&#8217;s where things become unintuitive: some API operations are not as obvious as they seem.</p><p>Let me give you an example:</p><p>There was a DynamoDB table with a lot of data, used for testing purposes, and it needed to be emptied regularly.</p><p>Unlike relational databases such as PostgreSQL, DynamoDB does not support a simple &#8220;DELETE * FROM table&#8221; operation. Instead, you must delete items in batches&#8212;up to 25 items at a time. This means looping through the table and deleting items in batches until it&#8217;s empty.</p><p>Sounds simple and cheap, right?</p><p>Not quite. If you perform a large number of write, read, or delete operations in a short burst, DynamoDB may throw throttling exceptions. But shouldn&#8217;t it scale effortlessly and almost infinitely, like the name &#8220;DynamoDB&#8221; suggests?</p><p>Yes, it does&#8212;but gradually, over time. While it scales to handle consistent high usage, it has limitations when dealing with sudden burst loads.</p><p>This can be mitigated by purchasing provisioned capacity to prepare the table for heavy usage. But this approach is inefficient&#8212;why pay for provisioned capacity all the time if you only empty the table once a week?</p><p>And here&#8217;s the next gotcha: in DynamoDB, you pay for each write and read operation. So, is deleting items free?</p><p>Unfortunately, it&#8217;s not. A delete operation counts as a write operation. Deleting a lot of items can quickly become expensive.</p><p>This highlights why relying on your intuition from on-premises systems can be risky when estimating costs in the public cloud.</p><p>Pro tip: If you run into this specific issue, there&#8217;s a loophole&#8212;deleting an entire DynamoDB table is free. So, if you want to delete all items from a table without incurring write operation costs, just delete the table and recreate it with the same settings.</p><p>And now you: What&#8217;s the most unintuitive cloud cost you&#8217;ve run into?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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[Don’t Be a Hero: Why Safe Migrations Beat Smart Hacks]]></title><description><![CDATA[A real-world lesson in why confidence isn&#8217;t a strategy.]]></description><link>https://blog.finnchristiansen.com/p/dont-be-a-hero-why-safe-migrations</link><guid isPermaLink="false">https://blog.finnchristiansen.com/p/dont-be-a-hero-why-safe-migrations</guid><dc:creator><![CDATA[Finn Christiansen]]></dc:creator><pubDate>Sat, 12 Jul 2025 06:37:29 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!LbDW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa095fad2-fced-4550-b357-9fc9c162b2ee_551x453.jpeg" 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_!LbDW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa095fad2-fced-4550-b357-9fc9c162b2ee_551x453.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LbDW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa095fad2-fced-4550-b357-9fc9c162b2ee_551x453.jpeg 424w, https://substackcdn.com/image/fetch/$s_!LbDW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa095fad2-fced-4550-b357-9fc9c162b2ee_551x453.jpeg 848w, https://substackcdn.com/image/fetch/$s_!LbDW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa095fad2-fced-4550-b357-9fc9c162b2ee_551x453.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!LbDW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa095fad2-fced-4550-b357-9fc9c162b2ee_551x453.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LbDW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa095fad2-fced-4550-b357-9fc9c162b2ee_551x453.jpeg" width="551" height="453" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a095fad2-fced-4550-b357-9fc9c162b2ee_551x453.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:453,&quot;width&quot;:551,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:63536,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.finnchristiansen.com/i/168131034?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa095fad2-fced-4550-b357-9fc9c162b2ee_551x453.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!LbDW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa095fad2-fced-4550-b357-9fc9c162b2ee_551x453.jpeg 424w, https://substackcdn.com/image/fetch/$s_!LbDW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa095fad2-fced-4550-b357-9fc9c162b2ee_551x453.jpeg 848w, https://substackcdn.com/image/fetch/$s_!LbDW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa095fad2-fced-4550-b357-9fc9c162b2ee_551x453.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!LbDW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa095fad2-fced-4550-b357-9fc9c162b2ee_551x453.jpeg 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 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>On a rainy Friday, your product owner asks you to do a complex database migration. After reading the requirements and acceptance criteria of the Jira ticket, you think: &#8220;F**k! This cannot be done easily!&#8221;</p><p>The problem is that the change is so complex, it cannot be done gracefully. You have to stop the backend, make a dump of the database, delete the old database, create a new database, and then run a complex migration script. Finally, you have to make sure that the backend works with the new database.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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><p>If anything breaks during the process, you have to roll everything back manually. Hopefully, that&#8217;s even possible.</p><p>What if the backend doesn&#8217;t work with the new database? Then you have to make the hard choice: spend all the effort and roll it back, or do ad-hoc changes to the backend in the middle of the night while you&#8217;re tired.</p><p>Even if this process works, you&#8217;ll be extremely exhausted &#8212; and this will have a negative impact on your private life during your free weekend days.</p><p>And if it doesn&#8217;t work? You&#8217;re screwed! Maybe you can summon a few colleagues and spend the rest of the weekend fixing it. Maybe you can&#8217;t fix it &#8212; and on Monday, you&#8217;ll be called by some angry customers. If your company is big, you&#8217;ll even read about it in the news.</p><p>The root cause of this is hubris: you think you&#8217;re a 10X developer who can do things with an all-at-once approach. Your genius, you believe, allows you to take a shortcut and avoid the rough road of a blue-green deployment with zero downtime.</p><div><hr></div><p>What would be the better approach?</p><p>Provision a new database in parallel with the existing one. Write to both databases simultaneously. Use the old schema for the old one and the new schema for the new one. Run a migration script that reads historical data from the old database, converts it to the new schema, and writes it to the new database. After a grace period, start reading from both databases and compare the results. If, after another grace period, the results are always the same, you can start using only the new database and archive the old one.</p><div><hr></div><p>So what&#8217;s the difference between the approaches?</p><p>The first is an all-at-once approach that relies on your genius as a 10X developer to take a leap of faith into the unknown. The second is a professional software development approach that does things incrementally, safely, and predictably.</p><div><hr></div><p>How do I know this? I&#8217;ve taken the leap of faith countless times. And in 99% of cases, I regretted it.</p><div><hr></div><p>Do you think confidence in tech is often mistaken for competence &#8212; especially when taking all-or-nothing risks?</p><p>Is it ever justifiable to go with an all-at-once approach in software &#8212; or is it always a sign of hubris?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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[Who Should Handle CORS: Your Backend or Your Infrastructure?]]></title><description><![CDATA[Why moving CORS handling to infrastructure helps in production &#8212; but hurts locally.]]></description><link>https://blog.finnchristiansen.com/p/who-should-handle-cors-your-backend</link><guid isPermaLink="false">https://blog.finnchristiansen.com/p/who-should-handle-cors-your-backend</guid><dc:creator><![CDATA[Finn Christiansen]]></dc:creator><pubDate>Thu, 10 Jul 2025 16:01:51 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!TW2N!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa27bf430-870d-4bed-a071-90c3d06f10fd_632x627.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_!TW2N!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa27bf430-870d-4bed-a071-90c3d06f10fd_632x627.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TW2N!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa27bf430-870d-4bed-a071-90c3d06f10fd_632x627.png 424w, https://substackcdn.com/image/fetch/$s_!TW2N!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa27bf430-870d-4bed-a071-90c3d06f10fd_632x627.png 848w, https://substackcdn.com/image/fetch/$s_!TW2N!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa27bf430-870d-4bed-a071-90c3d06f10fd_632x627.png 1272w, https://substackcdn.com/image/fetch/$s_!TW2N!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa27bf430-870d-4bed-a071-90c3d06f10fd_632x627.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TW2N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa27bf430-870d-4bed-a071-90c3d06f10fd_632x627.png" width="632" height="627" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a27bf430-870d-4bed-a071-90c3d06f10fd_632x627.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:627,&quot;width&quot;:632,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:48171,&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://blog.finnchristiansen.com/i/167999432?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa27bf430-870d-4bed-a071-90c3d06f10fd_632x627.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_!TW2N!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa27bf430-870d-4bed-a071-90c3d06f10fd_632x627.png 424w, https://substackcdn.com/image/fetch/$s_!TW2N!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa27bf430-870d-4bed-a071-90c3d06f10fd_632x627.png 848w, https://substackcdn.com/image/fetch/$s_!TW2N!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa27bf430-870d-4bed-a071-90c3d06f10fd_632x627.png 1272w, https://substackcdn.com/image/fetch/$s_!TW2N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa27bf430-870d-4bed-a071-90c3d06f10fd_632x627.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 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></p><p>A backend contains business logic and interfaces to the outside world. Oftentimes, the advice is to extract as many non-business-logic responsibilities from the backend application as possible and move them to standalone components in the infrastructure.</p><p>An example is CORS headers: every response from the backend to a client should contain the <code>Access-Control-Allow-Origin</code> header to inform the client (browser) whether the frontend is allowed to call the backend.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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><p>This can be handled in the backend. Most frameworks &#8212; like Hono &#8212; provide pre-built middleware for this purpose. Just call <code>app.use(cors())</code> in your main function, and everything will work.</p><p>Another approach is to let the load balancer (e.g., Nginx) set the headers. In this case, the backend application is not even aware of the concept of CORS.</p><p>If you want to compare the two approaches, you have to consider two cases:</p><ol><li><p>Deployed system (development and production environment in the cloud)</p></li><li><p>Local development setup</p></li></ol><p>Let&#8217;s start with the deployed system:</p><p>When a browser makes a request &#8212; for example, a POST request to the backend &#8212; and the backend is not on the exact same URL and port as the frontend, it will first send a <em>preflighted</em> <code>OPTIONS</code> request to the backend. The backend must respond &#8212; among other things &#8212; with the CORS headers like <code>Access-Control-Allow-Origin</code>. If everything is correct, the browser will then send the actual POST request.</p><p>Load balancers are capable of caching <code>OPTIONS</code> requests, so there will be far fewer requests to the backend.</p><p>Additionally, if there is a separate infrastructure team from the application team, setting the correct CORS headers could be enforced company-wide, and the application team doesn&#8217;t have to worry about it.</p><p>Now let&#8217;s look at the local development setup:</p><p>If you let the load balancer handle CORS, it won&#8217;t work when running your frontend and backend locally. Your browser will complain that there are no CORS headers. Thus, you have to set up a local load balancer like Nginx. In this setup, your frontend will call the load balancer, which will then forward the request to your backend.</p><p>This introduces more complexity.</p><p>How do you want to run the load balancer? As a system service in your Linux OS? As a Docker container? In a Docker Compose setup?</p><p>Who will configure and maintain the load balancer configuration? How do you keep it in sync across all developer machines?</p><p>If you use orchestrator frameworks like Vercel&#8217;s Turborepo for Node.js, it's more complicated to integrate the lifecycle of non-Node.js systems such as Nginx into the task dependency graph.</p><p>In production environments, it's generally preferable to handle CORS at the infrastructure level (e.g., via nginx or an API gateway). This centralizes configuration, allows for caching of preflight requests (if configured), and enforces organization-wide security policies without burdening the application team.</p><p>However, in local development, requiring a load balancer adds unnecessary complexity. Developers must manage extra tooling and keep configurations synchronized across environments. Instead, handling CORS within the backend application using middleware (like <code>app.use(cors())</code> in Hono) simplifies the development setup and improves the developer experience (DX).</p><p>Suggested approach:</p><p>Use a hybrid strategy:</p><ul><li><p>In <strong>development</strong>, implement CORS in the backend for simplicity.</p></li><li><p>In <strong>production</strong>, shift CORS to infrastructure for scalability, consistency, and better separation of concerns.</p></li></ul><p></p><p><strong>Do you think the infrastructure or the backend application should handle CORS?</strong></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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[Is It Really Urgent? Let Your Product Owner Decide!]]></title><description><![CDATA[Why developer priorities shouldn&#8217;t be set in private Slack messages.]]></description><link>https://blog.finnchristiansen.com/p/is-it-really-urgent-let-your-product</link><guid isPermaLink="false">https://blog.finnchristiansen.com/p/is-it-really-urgent-let-your-product</guid><dc:creator><![CDATA[Finn Christiansen]]></dc:creator><pubDate>Wed, 09 Jul 2025 18:38:42 GMT</pubDate><content:encoded><![CDATA[<p>On a random Monday between two meetings in the morning, you get a Slack direct message:</p><p>&#8220;Hi, Tom here from Team A. We need you to fix issue B immediately. We are using your API as an important dependency, and this blocks important feature F! Please do it ASAP!&#8221;</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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><p>Oh no!</p><p>You don&#8217;t want to block them, right?</p><p>It must be fixed, right?</p><p>It depends. Why? Let me explain:</p><p>Every task has an <strong>importance</strong> and an <strong>urgency</strong> in a specific context. The contexts are the company as a whole, Team A, and your own team. Importance and urgency have to be defined by the stakeholders and product owners.</p><p>Additionally, every person that messages you has a different ability to communicate importance and urgency. Sometimes people are just bad communicators, and it seems an issue is not important &#8212; although it is. Sometimes people have great communication skills and can introduce a great sense of urgency, although in the context of the whole company and your team, it is not. Maybe it isn&#8217;t even urgent in their own team &#8212; the person just wants to see their request fulfilled immediately for their own sake.</p><p>But isn&#8217;t this a bit theoretical?</p><p>There is a simple real-world solution if you are a developer: Make this discussion public and involve your product owner.</p><p>The product owner knows best each ticket&#8217;s importance and urgency and has the stakeholders on their short list. If you shift your tickets and follow the urge of some direct message, you make it harder for the product owner. Now the sprint is no longer as expected, and the board doesn&#8217;t reflect reality anymore.</p><p>How do you handle unexpected urgent requests in your team?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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["You should have thought about that before"]]></title><description><![CDATA[How a simple API call turned into a days-long delay]]></description><link>https://blog.finnchristiansen.com/p/you-should-have-thought-about-that</link><guid isPermaLink="false">https://blog.finnchristiansen.com/p/you-should-have-thought-about-that</guid><dc:creator><![CDATA[Finn Christiansen]]></dc:creator><pubDate>Mon, 07 Jul 2025 17:53:41 GMT</pubDate><content:encoded><![CDATA[<p>Let&#8217;s say you want to implement a new feature in your backend. You have to call the API of another team. It&#8217;s only a small lookup &#8212; e.g., you provide the address of a user and you get back the username.</p><p>Simple task, right? Let&#8217;s start by adding an endpoint to the backend. Implement a service class in a best-practice TDD way. Of course, for best-practice unit tests, you have to mock the API of the other team. But that&#8217;s the way to go, right?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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><p>You create a PR and ask for a review. Your product owner asks you how long it will take. Surely, you just have to wait for the review &#8212; but you promise it will be done by the end of business.</p><p>Finally, after a few back-and-forth cycles with the reviewer, you get the approval. Just merge it and wait a few minutes for the CI to do its work.</p><p>After it is finally deployed, you call your backend&#8217;s API for the first time. Just this final integration test and you can call it a day.</p><p>But nothing happens&#8230;</p><p>Why?!?</p><p>After 5 seconds, Postman runs into a timeout.</p><p>The next day, the product owner expects your feature to work. But it doesn&#8217;t.</p><p>&#8220;The other team&#8217;s API is in a different AWS account. Have you requested a firewall change to make it accessible from our account?&#8221;</p><p>Oh no! This is the missing piece! Why didn&#8217;t you think about this in advance? Wasn&#8217;t it obvious that this needed to be done as the very first item?</p><p>The process of changing a firewall takes days, and the only way to finish your feature on time would have been to wait for the changes <em>while</em> implementing the feature &#8212; and not afterwards.</p><p>Could you have done something better?</p><p>Afterwards, everyone is always an expert stock trader. But the question is not whether the choice was right in retrospect (<em>a posteriori</em>), but from the point of view at the time the choice was made.</p><p>The question is: If you are a backend developer for applications in AWS, is there a list of things you have to check before starting implementation, to avoid long waiting times and external dependencies <em>after</em> the implementation is done?</p><p>I don&#8217;t know the answer &#8212; because by definition, I don&#8217;t know the unknown unknowns. But let me try to give you some points:</p><ul><li><p>Are there external dependencies?</p></li><li><p>Can you access the external API from a network point of view?</p></li><li><p>Do you have the credentials to access the external API?</p></li><li><p>How many requests per second will your backend handle? Does the external API allow this rate, or will you run into a rate limit?</p></li><li><p>And so on.</p></li></ul><p>Now the question is: If there is such a list, how long is it? If it&#8217;s too long, it might be less work to just fix the issues with the integration afterwards than to always go through a long checklist.</p><p>What do you think? Can most of the problems in the integration of components be avoided by going through a determined list of known stumbling blocks?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.finnchristiansen.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">Thanks for reading! Subscribe for free to receive new posts and support my work.</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></channel></rss>