<?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[Joseph Gefroh: Engineering Craftsmanship]]></title><description><![CDATA[Where I talk about the craft of software engineering.]]></description><link>https://blog.jgefroh.com/s/software-engineering-craftmanship</link><image><url>https://substackcdn.com/image/fetch/$s_!sphd!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cef45a3-7420-4cba-95f3-46a3b5d34293_100x100.png</url><title>Joseph Gefroh: Engineering Craftsmanship</title><link>https://blog.jgefroh.com/s/software-engineering-craftmanship</link></image><generator>Substack</generator><lastBuildDate>Tue, 07 Apr 2026 07:44:00 GMT</lastBuildDate><atom:link href="https://blog.jgefroh.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Joseph Gefroh]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[jgefroh@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[jgefroh@substack.com]]></itunes:email><itunes:name><![CDATA[Joseph Gefroh]]></itunes:name></itunes:owner><itunes:author><![CDATA[Joseph Gefroh]]></itunes:author><googleplay:owner><![CDATA[jgefroh@substack.com]]></googleplay:owner><googleplay:email><![CDATA[jgefroh@substack.com]]></googleplay:email><googleplay:author><![CDATA[Joseph Gefroh]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[It's just a textbox]]></title><description><![CDATA[On complexity, success, and product engineering.]]></description><link>https://blog.jgefroh.com/p/its-just-a-textbox</link><guid isPermaLink="false">https://blog.jgefroh.com/p/its-just-a-textbox</guid><dc:creator><![CDATA[Joseph Gefroh]]></dc:creator><pubDate>Fri, 24 Jan 2025 18:51:08 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/489ad75f-5243-4327-8d33-642f2b43512c_744x296.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_!H4YI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9767c7fe-9853-44f2-8367-cd96019a04d9_744x296.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!H4YI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9767c7fe-9853-44f2-8367-cd96019a04d9_744x296.png 424w, https://substackcdn.com/image/fetch/$s_!H4YI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9767c7fe-9853-44f2-8367-cd96019a04d9_744x296.png 848w, https://substackcdn.com/image/fetch/$s_!H4YI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9767c7fe-9853-44f2-8367-cd96019a04d9_744x296.png 1272w, https://substackcdn.com/image/fetch/$s_!H4YI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9767c7fe-9853-44f2-8367-cd96019a04d9_744x296.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!H4YI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9767c7fe-9853-44f2-8367-cd96019a04d9_744x296.png" width="744" height="296" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9767c7fe-9853-44f2-8367-cd96019a04d9_744x296.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:296,&quot;width&quot;:744,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:11131,&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;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!H4YI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9767c7fe-9853-44f2-8367-cd96019a04d9_744x296.png 424w, https://substackcdn.com/image/fetch/$s_!H4YI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9767c7fe-9853-44f2-8367-cd96019a04d9_744x296.png 848w, https://substackcdn.com/image/fetch/$s_!H4YI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9767c7fe-9853-44f2-8367-cd96019a04d9_744x296.png 1272w, https://substackcdn.com/image/fetch/$s_!H4YI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9767c7fe-9853-44f2-8367-cd96019a04d9_744x296.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>When you&#8217;re creating brand new software, life is simple. Your focus is exclusively on the functionality of the software. It&#8217;s a care-free, liberating world. Coding is an act of pure creation. If you can think it, you can build it, and typically very quickly. You could see a change in production just a minute or two.</p><p>There&#8217;s so much you don&#8217;t have to think about or acknowledge, <em>at all</em>.</p><p>Your focus is exclusively on the behavior of the thing you are trying to build. It&#8217;s like running through a green field with your eyes closed. You can run full sprint ahead and absolutely fly without worrying about hitting anything.</p><p>If you want to add a new page, go ahead! If you want to add a textbox somewhere, you can! Just do it. </p><p>In this green field, a textbox is just a textbox. It only takes a minute to add it.</p><p><strong>Then, you start seeing success.</strong></p><p>People start using your software. People start charging money for your software. Your software starts to become a <em>product</em>. A business forms around it. Customers start requesting things, and more importantly: they start depending on its continued functioning and existence. </p><p>People start asking for more. More capabilities. More features. More connections with other things - things that were never meant to be connected to. Other people start making decisions for thing you&#8217;re building. You can&#8217;t even call them people anymore, now they&#8217;re called <em>stakeholders</em>. The green field becomes a tangled bramble of needs and requests. It becomes muddy. </p><h2>The brown field</h2><p>In this brown field, the textbox is no longer &#8220;just a textbox&#8221;. There&#8217;s so much more to it. There&#8217;s business rules, often lost in nuance. </p><p>Rules around:</p><ul><li><p>How it&#8217;s displayed - masking, borders, colors, shapes, sizes, positions, margins</p></li><li><p>How it&#8217;s tracked - analytics, auditing, tracking who, when, and where</p></li><li><p>How it&#8217;s accessible - screenreaders, mobile, translations, color-blind support</p></li><li><p>How it&#8217;s secured - character limits, XSS prevention, rate limiting, authorization</p></li><li><p>How it&#8217;s validated - input rules, how errors display,</p></li><li><p>How it&#8217;s expected - stakeholder training, user habits, data migrations</p></li><li><p>How it&#8217;s operated - cost control for usage-based vendors, feature flags for visbility</p></li><li><p>How it&#8217;s maintained - rules in one usage vs. another, reusability of its functionality</p></li><li><p>How it&#8217;s observed - error handling and reporting, notifications if it breaks</p></li><li><p>How it&#8217;s communicated - loading indicators, animations, user delight, labeling</p></li><li><p>How it&#8217;s scaled - paint and render performance, server optimizations</p></li><li><p>How it&#8217;s effective - A/B testing of impact, pre-filling of data, ease of completion</p></li><li><p>How it&#8217;s used -  if users understand it, if they make mistakes, if they&#8217;re faster</p></li><li><p>How it&#8217;s valued - why customers want it, why the business needs it, if it matters</p></li><li><p>&#8230;and so much more.</p></li></ul><p><strong>The textbox isn&#8217;t just a textbox anymore.</strong> It&#8217;s an entire ecosystem with a long history of past decisions, some forced by the context, and others made by dozens of people all without awareness of the other decisions. It&#8217;s connected to other elements with just as much complexity, all likewise interconnected in mysterious, likely undocumented ways. Some of those decisions may be written down or remembered, others not.</p><h2>Hidden complexity</h2><p>All of that complexity isn&#8217;t visible to any single person. </p><p>If you aren&#8217;t a marketer, you won&#8217;t notice that it can be targeted by the tag manager. If you aren&#8217;t visually impaired, you won&#8217;t notice it has screen reader support. If you aren&#8217;t using an lower-end machine you won&#8217;t notice it&#8217;s been made faster. If you don&#8217;t speak another language, you won&#8217;t notice it has translation. If you aren&#8217;t coming from another site, you won&#8217;t notice it pre-populates. If you aren&#8217;t the finance leader, you won&#8217;t notice the revenue impact. If you aren&#8217;t the auditor, you won&#8217;t notice the DOM has a label that&#8217;s technically a violation of a long forgotten rule.</p><p>Yet, you as the engineer still have to consider it all. You have to reckon with and factor in these decisions as you define and implement change after change after change - all under what always seems to be a tighter and tighter deadline. </p><p>In the green field, hours was enough to build an entire product. In the brown field, it&#8217;s sometimes barely enough to understand what a request is even asking for.</p><p>You have to make changes within the constraints of the past and future, known and unknown. Changes will eventually break something, and the affected party will fill a bug report which you have to then complete and fix.</p><p><strong>That&#8217;s the price of success</strong> - people use the software and notice when it doesn&#8217;t behave according to how they think it should.</p><div><hr></div><h2>How should it behave?</h2><p>It&#8217;s the hardest question you might be able to ask someone at a company that&#8217;s been around for a while.</p><p>The stakeholders in the company won&#8217;t be able to define it for you. Nobody actually knows the intricacies - not in full. <em>You have to discover it.</em> </p><p>So, you talk to users to find our their needs. You analyze how people are using the product. Where they get stuck. Where they click and don&#8217;t click. Where they enter. Where they leave. How all that translates into the how the user gets their job done and the company accomplishing their goals.</p><p>But, that&#8217;s still not enough. </p><p>Knowing what you intend the software to do is just part of the problem. You also have to know what it was intended to do and how it works today. </p><p>There&#8217;s no easy map - just code and history. To find it, you have to do good old fashioned archaeology. You search commit histories, Slack discussions, old documentation, random meeting notes, talk to co-workers, view logs. Bit by bit, you&#8217;re able to build <em>context</em> that makes your follow-on decisions implementing more accurate.</p><h2>Speed vs. Accuracy</h2><p>The challenge is - context building takes time. It&#8217;s time away from developing and coding. It affects the speed at which you can deliver the change. It might be too slow for the tastes of the company or your manager. You might even feel you aren&#8217;t really an engineer anymore.</p><p>When faced with the prospects of having to have &#8220;conversations about your throughput&#8221;, you might try to reverse course: moving faster, deciding faster, acting faster. </p><p>Rookie mistake - all that does is just cause you to miss some hidden interconnection nobody knew about and cause an incident. Now, you just traded the throughput conversation for one about quality.</p><p>At this point it&#8217;s easy to get frustrated. The natural tendency of an engineer is to push back. You might start to reject tickets due to &#8220;incomplete requirements&#8221;, or start approaching every change request like a contract negotiation, requiring volumes of forms to be defined and filled out. </p><p><strong>That approach won&#8217;t work.</strong> Asking others to define it won&#8217;t help because <em>they don&#8217;t know.</em> They don&#8217;t have the knowledge, nor do they have the time. As the engineer, you at least have the advantage of being able to look under the hood and live in the product. For some of your co-workers, they only think about the product for 30 seconds a day - they have other day jobs they have to do.</p><p>It&#8217;s up to you to figure it out. The more you dive into it, the more you can grow and improve. Embrace the breadth. It&#8217;s not just about the technology.</p><h2>Becoming a product engineer</h2><p>When you&#8217;re a product engineer, you have to &#8220;wear a lot of hats&#8221;. The product hat. The finance hat. The user hat. The engineering hat. The investor hat. The customer hat. </p><p>None of them will seem like they fit.</p><p>You might find it hard to think about things from each of the perspectives. To wear each hat and truly adopt that persona as your own, even just for a minute. To think through a problem as if you were the person the hat was made for.</p><p>Well, nobody said Product Engineering was easy. </p><p>It&#8217;s difficult. It&#8217;s high pressure. You&#8217;ll constantly hear questions from stakeholders asking &#8220;<em>Why can&#8217;t we go faster?&#8221;</em> and <em>&#8220;Why can&#8217;t we stop breaking things?&#8221;</em> They might even start comparing you to other teams and other companies, not realizing those teams are on newer products and in greener fields.</p><p>It&#8217;s easy to feel stuck - to feel that any move you make is going to be wrong. </p><p><strong>Grind away at it anyways.</strong> In situations like this, the only winning move is to lose and learn. </p><p>The mistakes will happen. Focus on improving, learning, and sharing knowledge. Focus on getting the job done. </p><p>Little by little, it&#8217;ll get easier. The patterns will get clearer. The cause and effect will start becoming automatic. The chaos will turn into complexity which will turn into simplicity.</p><p>You&#8217;ll start thinking deeply. Thinking about how the users will interact with the change. How it&#8217;ll impact the business. What they&#8217;ll expect. What they&#8217;ll want. What they&#8217;ll need. It&#8217;ll just become automatic.</p><p>You start being able to avoid problems entirely, and<strong> </strong>to find insights that have a huge impact. It&#8217;ll become second nature to think about things like accessibility and scalability while thinking about revenue and compliance and usability. You&#8217;ll be able to see the thread of how the click will translate across 20 hops to impact the bottom line. </p><p>It&#8217;ll all makes sense. It&#8217;ll all connect. You see it in your head, with every decision you make. </p><h2>Being an expert</h2><p>The next time you need to add a textbox, you&#8217;ll be more confident. You&#8217;ll consider the impact - to everyone: users, customers, the business, your peers, your system, your product, yourself. The non-functional requirements that many don&#8217;t even know exist, you&#8217;ll just consider through your minute-by-minute actions. You feel like you&#8217;ve got this. You&#8217;ll <em>know</em> you&#8217;ve got this. You&#8217;ll be wiser, faster, better.</p><p>It&#8217;ll take just a minute to add the textbox: a long journey to arrive at the same destination. </p><p>But not quite. This time, you&#8217;ll have considered the &#8220;-ilities&#8221;. You&#8217;ll have considered the things not mentioned. You&#8217;ll have done the things not thought about. You&#8217;ll have considered things that aren&#8217;t relevant now, but will be soon. You&#8217;ll have factored in the past, present, and future. </p><p>You&#8217;ll complete your ticket - not what was defined, not even what was intended, but what was <em>needed</em>. Whether the field is green or brown won&#8217;t matter anymore.</p><p>You&#8217;ll be happy with the result. You&#8217;ll have made it look easy. It actually will be easy.</p><p>After all, it&#8217;s just a textbox.</p><div class="paywall-jump" data-component-name="PaywallToDOM"></div>]]></content:encoded></item><item><title><![CDATA[Software Engineering Techniques— Avoid leaking sensitive information with a tripwire]]></title><description><![CDATA[Avoid the all-to-common developer mistake of leaking sensitive information in an API.]]></description><link>https://blog.jgefroh.com/p/software-engineering-techniques-avoid</link><guid isPermaLink="false">https://blog.jgefroh.com/p/software-engineering-techniques-avoid</guid><dc:creator><![CDATA[Joseph Gefroh]]></dc:creator><pubDate>Sun, 11 Feb 2024 18:30:51 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!d9FD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21ffdaeb-6256-4123-99e4-17cdcc200876_1312x574.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_!d9FD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21ffdaeb-6256-4123-99e4-17cdcc200876_1312x574.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!d9FD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21ffdaeb-6256-4123-99e4-17cdcc200876_1312x574.png 424w, https://substackcdn.com/image/fetch/$s_!d9FD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21ffdaeb-6256-4123-99e4-17cdcc200876_1312x574.png 848w, https://substackcdn.com/image/fetch/$s_!d9FD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21ffdaeb-6256-4123-99e4-17cdcc200876_1312x574.png 1272w, https://substackcdn.com/image/fetch/$s_!d9FD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21ffdaeb-6256-4123-99e4-17cdcc200876_1312x574.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!d9FD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21ffdaeb-6256-4123-99e4-17cdcc200876_1312x574.png" width="1312" height="574" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/21ffdaeb-6256-4123-99e4-17cdcc200876_1312x574.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:574,&quot;width&quot;:1312,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:379957,&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;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!d9FD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21ffdaeb-6256-4123-99e4-17cdcc200876_1312x574.png 424w, https://substackcdn.com/image/fetch/$s_!d9FD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21ffdaeb-6256-4123-99e4-17cdcc200876_1312x574.png 848w, https://substackcdn.com/image/fetch/$s_!d9FD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21ffdaeb-6256-4123-99e4-17cdcc200876_1312x574.png 1272w, https://substackcdn.com/image/fetch/$s_!d9FD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21ffdaeb-6256-4123-99e4-17cdcc200876_1312x574.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>A problem I&#8217;ve seen a lot is the back-end controller accidentally serializing and returning confidential data to the front-end.</p><p>Sensitive things like password hashes, API access credentials, etc. may accidentally be serialized by magic framework defaults.</p><h1><strong>Promiscuous serialization</strong></h1><p>It&#8217;s quite easy to accidentally do, especially on a large team with multiple projects. A lot of frameworks put convenience over security, serializing all fields by default in the absence of a whitelist.</p><p>As a result, common developer errors may contribute to hidden security vulnerabilities.</p><p>In Ruby on Rails, for example, not specifying a serializer when returning an object from a controller will simply return <em>all</em> of the attributes for that object.</p><pre><code>class UsersController &lt; ActionController:Base
  
  # If there is no UsersSerializer, this method leaks sensitive data  def show
    user = User.find(params[:id])
    render user
  endend</code></pre><p>This might happen in a lot of situations. A developer may forget to add the serializer. You may be working with a name-spaced model and the auto-magic resolution doesn&#8217;t kick in just right.</p><p>Even if you do specify a serializer, you&#8217;re not entirely safe. If that serializer uses relationships in the serializer, those relationships will have all of their attributes serialized as well:</p><pre><code>class UsersController
  
  def show
    User.find(params[:id])
  endendclass UsersSerializer
  
  attributes :id, :name
  has_many :secret_notes # leak!end</code></pre><p>You can see there&#8217;s a lot of potential for data to be leaked &#8212; on larger teams it is inevitable for something to happen eventually.</p><p>How do you have secure defaults in an environment that fights against you?</p><p>Simple &#8212; use a tripwire.</p><h1><strong>A tripwire?</strong></h1><p>Tripwires are things that have an effect upon a certain trigger.</p><p>In this basic implementation, the tripwire is a field that, when serialized, will throw an error. You can do this in in the following way (example using Ruby on Rails):</p><div class="paywall-jump" data-component-name="PaywallToDOM"></div><ul><li><p>Add an automatically serialized column to the table you want to protect</p></li><li><p>In the model, create a getter that throws an error</p></li></ul><pre><code>class User &lt; ActiveRecord::Base   def tripwire
       raise 'default serialization is not allowed'
   endend</code></pre><p>When the framework attempts to serialize this model using the default promiscuous method, it will encounter this field and throw an error, alerting the developer that something went wrong.</p><p>This tripwire optimizes the system in favor of failing early and preventing the leaking of sensitive information. It may result in an increase in errors if it ever makes it to production, but will prevent the greater issue of leaking sensitive data. Fail-open can lead to all sorts of confidentiality breaches, whereas the fail-closed technique of the tripwire can prevent these from happening at the cost of potentially preventing data access in legitimate situations.</p>]]></content:encoded></item><item><title><![CDATA[YAGNI and DRY — the KISS of Death for Your Software Project]]></title><description><![CDATA[Learn why popular programming mantras like YAGNI, DRY, and KISS can be as destructive as they are seductive.]]></description><link>https://blog.jgefroh.com/p/yagni-and-dry-the-kiss-of-death-for</link><guid isPermaLink="false">https://blog.jgefroh.com/p/yagni-and-dry-the-kiss-of-death-for</guid><dc:creator><![CDATA[Joseph Gefroh]]></dc:creator><pubDate>Sun, 11 Feb 2024 18:27:31 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!d6Q8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f8cd040-f4b5-4518-9321-9ea14f8a3657_1346x518.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_!d6Q8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f8cd040-f4b5-4518-9321-9ea14f8a3657_1346x518.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!d6Q8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f8cd040-f4b5-4518-9321-9ea14f8a3657_1346x518.png 424w, https://substackcdn.com/image/fetch/$s_!d6Q8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f8cd040-f4b5-4518-9321-9ea14f8a3657_1346x518.png 848w, https://substackcdn.com/image/fetch/$s_!d6Q8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f8cd040-f4b5-4518-9321-9ea14f8a3657_1346x518.png 1272w, https://substackcdn.com/image/fetch/$s_!d6Q8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f8cd040-f4b5-4518-9321-9ea14f8a3657_1346x518.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!d6Q8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f8cd040-f4b5-4518-9321-9ea14f8a3657_1346x518.png" width="1346" height="518" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0f8cd040-f4b5-4518-9321-9ea14f8a3657_1346x518.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:518,&quot;width&quot;:1346,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:837523,&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;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!d6Q8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f8cd040-f4b5-4518-9321-9ea14f8a3657_1346x518.png 424w, https://substackcdn.com/image/fetch/$s_!d6Q8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f8cd040-f4b5-4518-9321-9ea14f8a3657_1346x518.png 848w, https://substackcdn.com/image/fetch/$s_!d6Q8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f8cd040-f4b5-4518-9321-9ea14f8a3657_1346x518.png 1272w, https://substackcdn.com/image/fetch/$s_!d6Q8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f8cd040-f4b5-4518-9321-9ea14f8a3657_1346x518.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>Software development is full of mantras that are chanted by developers of all levels as prima facie evidence that justifies and proves the rationality of their decisions.</p><p>Who amongst us hasn&#8217;t heard people or told people to YAGNI, DRY, or KISS? They&#8217;re popular mantras because, when followed properly, they work to guide and nudge thinking towards more effective development and programming.</p><p><strong>However, they have a dark side.</strong></p><p>Because these terms are intentionally vague and broad, they can be misapplied or over-applied in many situations. These memes are then used as justification to continue applying the wrong approach to a problem.</p><p>They&#8217;re hard to argue against because philosophically these generalized statements are correct. We want to believe them. We have to believe them. To not believe them makes one feel as if they are not a developer. This makes them difficult to argue against, combined with the fact that they contain only the broadest of truths and are not applicable in every situation.</p><h1><strong>KISS &#8212; Keep it Simple, Stupid</strong></h1><p>Despite hurting feels across the world, KISS, or &#8220;keep it simple, stupid&#8221; is often used to promote simplicity and prevent the over-engineering tendencies many engineers have.</p><p>There might be a hundred ways to implement any one requirement. KISS implies that the simplest method is often the best in terms of complexity, speed, and cost.</p><p>For example &#8212; an engineer may need to implement a way to change the program configuration. They may be evaluating two different approaches: read from the database, or read from a file.</p><p>Developers apply KISS in this situation: avoid complexity and dependencies caused by the additional database tables by choosing the simpler method of reading from a file.</p><h2><strong>What&#8217;s wrong with it?</strong></h2><p>The dark side of KISS is that is can lead you down the path of the <em>minimum</em>, which if applied over time eventually results in subpar outcomes.</p><p>Let&#8217;s suppose a second requirement follows that the configuration needs to be changeable during runtime by internal employees.</p><p>A KISS-focused iteration after this could be to give internal administrators access to the server to change the configuration file. However, this goes down a path that results in more ineffective results than you would have had you taken the slightly more complex option of putting it in a database table.</p><p>KISS is also often used to defend under-developed solutions and gloss over implicit or unspoken requirements. In the example above, one of the unspoken requirements is that we don&#8217;t want the server to go down if we changed the configuration. However, because we&#8217;ve gone down the route offered by KISS, this constraint is not even mentioned or addressed.</p><p>Sometimes situations involve real complexity, and in these situations, KISS can dangerously scope out important considerations.</p><h1><strong>YAGNI &#8212; You Aren&#8217;t Gonna Need It</strong></h1><p>YAGNI is often used to prevent engineers from over-engineering their solutions in an effort to address situations or requirements that don&#8217;t currently need to be addressed.</p><p>Let&#8217;s face it &#8212; engineers have a tendency to overbuild for use cases they will never see, or inappropriately delay value delivery for minimal gains in architectural and code cleanliness &#8212; code-golf is a thing, as are ivory towers.</p><p>YAGNI is the mantra that acts as the counter-balance to the path of infinite &#8220;what if?&#8221; questions that engineers can travel down. It prevents scope creep that comes from attempting to address the problems of an uncertain future that are better left to be solved for when they actually occur (if they ever).</p><h2><strong>What&#8217;s wrong with it?</strong></h2><p>YAGNI has a dark side. It is often used by engineers as a reason for under-engineering in a misguided attempt to justify sloppy work with nebulous promises of unverifiable gains in development speed.</p><p><strong>How do you tell if it is being misapplied? </strong>When YAGNI is used as the reason for NOT doing something, it&#8217;s time to ask yourself some questions:</p><ul><li><p>Is it being used to avoid solving a problem that is inherent to the domain?</p></li><li><p>Is it being used to avoid developing something inherently foundational to the product or software (eg. authorization for web applications)?</p></li><li><p>Are speed benefits from avoiding the additional work being tracked to ensure it actually leads to faster development?</p></li><li><p>Is the cost of doing it or implementing it actually as costly as believed?</p></li></ul><p>YAGNI can be a powerful guiding principle when applied judiciously. However, it can also be used as a crutch to excuse sloppy development and low quality work in the name of faster delivery speeds that never materialize.</p><h2><strong>A tale of a highly detailed project</strong></h2><p>I once worked on a project that had almost no abstractions. Instead, every line of code was at the lowest possible level of detail. Functions were thousands of lines long, forcing you to read every line to understand what it was doing. There were few classes or reusable libraries.</p><p>The team was hesitant to create classes or even move code into functions. They believed it was over-engineering and yelled &#8220;YAGNI&#8221;: why move code into a function when it wouldn&#8217;t be called by anything else? Why create abstractions we didn&#8217;t need when copy-pasting was so much faster?</p><div class="paywall-jump" data-component-name="PaywallToDOM"></div><p>I had to remind them that engineering was about developing primitives and abstractions that hide the details so that you can focus on more important concepts and ideas.</p><p>Software is founded on abstractions: from on/offs to bits to registers to memory management to instructions to code to functions to classes to libraries to programs to services. Without these and other abstractions, our minds are forced to contend with details that aren&#8217;t relevant for what we&#8217;re trying to do, greatly distracting from solving the real problem at hand.</p><p>More often than not, these details don&#8217;t matter.</p><h2><strong>An alternative &#8212; You Are Gonna Need It</strong></h2><p>Spend a bit of extra time thinking about and modeling the technical and business domain. If it is some common function such as logins, file downloads, or auditing, think about how you would implement it, and then implement something minimal that will move you towards the ideal solution.</p><p>Remember &#8212; the value of the plan is in the planning. Forcing yourself to think about all of the &#8220;What Ifs?&#8221; will help ensure you don&#8217;t paint yourself into a corner that will prevent the code from evolving to support changing requirements. It doesn&#8217;t mean you have to everything at once &#8212; you can still ensuring you move in small, incremental steps.</p><p>This will save a tremendous amount of resources in the long-run, especially if it is something foundational to future development efforts, such as code related to UI components, design systems, authorization, endpoints, or data layers. These are things that are common and highly likely to be needed as the product evolves.</p><p>If you solve a problem just once and write the solution in a way where you can use it anywhere else you encounter the problem, you have effectively saved an exponential amount of time from future development.</p><h1><strong>DRY &#8212; Don&#8217;t Repeat Yourself</strong></h1><p>DRY is all about not doing the same thing over and over and over and over and over and over and over. It encourages developers to think about commonalities and not just robotically copy-paste functionality.</p><p>By emphasizing centralization of shared functionality, teams can more easily keep code volume in check, preventing a cascading propagation of changes should a commonly copied piece of code require modification.</p><h2><strong>What&#8217;s wrong with it?</strong></h2><p>DRY casts a terrible spell on developers who take it too far and centralize code that has the same form but not the same purpose. They take code that might be structurally similar and tie use cases together, causing a change in one to lead to an unintended or undesired change in the other. I see this often in React/Redux developers who use Redux to avoid having to think about state management.</p><p>This unintended globalization of previously localized data can lead to a lot of negative consequences.</p><h2><strong>A tale of a User</strong></h2><p>In one project I worked on, we had a a list of users on the front-end. The developers were judiciously following DRY, and placed these values in a centralized cache for use by all of the other pages, including two in particular: a user list page and a user lookup search.</p><p>This proved to be a mistake. Even though both components happened to use a list of users, they had completely different requirements and sources of change for that data:</p><ul><li><p>The user list page wanted many additional details in the payload, whereas the user lookup wanted just the name and ID.</p></li><li><p>The user list page contained PII such as full names and email addresses, whereas the user lookup only contained minimal public information.</p></li><li><p>The user list page was manually editable and sortable, whereas the lookup was not.</p></li></ul><p>Because these two shared the same centralized data store, it wreaked havoc on the front-end. Sometimes data was missing. Other times items came in out of order. Sometimes using the user lookup caused the user list to disappear. It was a mess.</p><p>The sources of truth for these pieces of data should have been completely different, but they had been DRY&#8217;d up and combined.</p><h2><strong>An alternative &#8212; Do Repeat Yourself</strong></h2><p>Whenever you store or use a piece of data or write some code, always ask yourself the following questions:</p><ul><li><p>What are the reasons this code or data may change, and do those reasons apply to this situation?</p></li><li><p>What use cases are leveraging this code or data, and is this shared piece generic enough to suit both cases without impacting either one?</p></li><li><p>What stakeholders would request changes to this piece of functionality, and could they simultaneously both be right and disagree?</p></li></ul><p>If you identify that the form may be similar but the purposes or reasons different, give yourself permission to repeat yourself.</p><h1><strong>The Lesson</strong></h1><p>Be judicious in your application of mantras. While they can help guide thinking and are useful rules of thumb, they were never intended to be dogmas.</p><p><strong>Don&#8217;t be a lazy thinker. </strong>Always be cognizant of the &#8220;Why?&#8221; behind what you are doing, and know when to break the rules.</p>]]></content:encoded></item><item><title><![CDATA[How to Design Software — Monitoring Systems]]></title><description><![CDATA[Have you ever seen products with status pages or automated monitoring? Ever wonder how it all works and how to make your own? Learn how!]]></description><link>https://blog.jgefroh.com/p/how-to-design-software-monitoring-systems-e4313c59629e</link><guid isPermaLink="false">https://blog.jgefroh.com/p/how-to-design-software-monitoring-systems-e4313c59629e</guid><dc:creator><![CDATA[Joseph Gefroh]]></dc:creator><pubDate>Sun, 06 Feb 2022 18:51:43 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/5a1c0fab-8deb-4c13-93de-66b65e2fd1b8_800x418.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Fup_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd332698-ac32-450f-858a-f2d7ed459628_800x418.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Fup_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd332698-ac32-450f-858a-f2d7ed459628_800x418.png 424w, https://substackcdn.com/image/fetch/$s_!Fup_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd332698-ac32-450f-858a-f2d7ed459628_800x418.png 848w, https://substackcdn.com/image/fetch/$s_!Fup_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd332698-ac32-450f-858a-f2d7ed459628_800x418.png 1272w, https://substackcdn.com/image/fetch/$s_!Fup_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd332698-ac32-450f-858a-f2d7ed459628_800x418.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Fup_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd332698-ac32-450f-858a-f2d7ed459628_800x418.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bd332698-ac32-450f-858a-f2d7ed459628_800x418.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Fup_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd332698-ac32-450f-858a-f2d7ed459628_800x418.png 424w, https://substackcdn.com/image/fetch/$s_!Fup_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd332698-ac32-450f-858a-f2d7ed459628_800x418.png 848w, https://substackcdn.com/image/fetch/$s_!Fup_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd332698-ac32-450f-858a-f2d7ed459628_800x418.png 1272w, https://substackcdn.com/image/fetch/$s_!Fup_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd332698-ac32-450f-858a-f2d7ed459628_800x418.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><h4>Explore the conceptual architecture of monitoring systems and learn how to build your own system observation engine.</h4><p>Have you ever seen products with <a href="https://medium.statuspage.io/">status pages</a>? Ever wonder how it all works and how to make your own? Most companies don&#8217;t do it themselves&#8202;&#8212;&#8202;they use a 3rd-party vendor like <a href="https://StatusPage.io">StatusPage</a> combined with automated reporting tools like <a href="https://www.pingdom.com/">Pingdom</a>. If you&#8217;re using this in a production environment, chances are you&#8217;ll want to use a vendor.</p><p>However, if you&#8217;re curious about design approach and possible ways to build something like this on the quick, then this is the article for you!</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qoPE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff928361a-640a-46f9-a720-f9689f2b2c39_800x359.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qoPE!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff928361a-640a-46f9-a720-f9689f2b2c39_800x359.png 424w, https://substackcdn.com/image/fetch/$s_!qoPE!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff928361a-640a-46f9-a720-f9689f2b2c39_800x359.png 848w, https://substackcdn.com/image/fetch/$s_!qoPE!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff928361a-640a-46f9-a720-f9689f2b2c39_800x359.png 1272w, https://substackcdn.com/image/fetch/$s_!qoPE!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff928361a-640a-46f9-a720-f9689f2b2c39_800x359.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qoPE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff928361a-640a-46f9-a720-f9689f2b2c39_800x359.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f928361a-640a-46f9-a720-f9689f2b2c39_800x359.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!qoPE!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff928361a-640a-46f9-a720-f9689f2b2c39_800x359.png 424w, https://substackcdn.com/image/fetch/$s_!qoPE!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff928361a-640a-46f9-a720-f9689f2b2c39_800x359.png 848w, https://substackcdn.com/image/fetch/$s_!qoPE!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff928361a-640a-46f9-a720-f9689f2b2c39_800x359.png 1272w, https://substackcdn.com/image/fetch/$s_!qoPE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff928361a-640a-46f9-a720-f9689f2b2c39_800x359.png 1456w" sizes="100vw"></picture><div></div></div></a><figcaption class="image-caption">A screenshot of a monitoring page.</figcaption></figure></div><h3>Firstly&#8202;&#8212;&#8202;a conceptual exploration of &#8220;Monitoring&#8221;</h3><p>The purpose of a monitoring system is simple: determine whether something is working or not.</p><p>That statement has a bit more nuance than meets the eye, so let&#8217;s take a closer look.</p><h4>What are you monitoring?</h4><p>It&#8217;s important to recognize that the &#8220;something&#8221; that gets monitored can be <em>anything</em>. Oftentimes, it is a system within your control, such as an application server. Other times, it is a vendor system. Perhaps it is a mix of both, such as a technical process that needs to contact a vendor system.</p><p>Approaching it from the perspective that the monitored target is <em>arbitrary</em> keeps your thinking from going down the path of a specific implementation.</p><h4>What does it mean to&nbsp;&#8220;work&#8221;?</h4><p>A system that is working can mean a lot of different things. The most binary definition is whether the system is up (working) or down (not working).</p><p>That&#8217;s an incomplete answer, though.</p><ul><li><p>If the system is up, but not doing what it was intended to, does it mean it is working?</p></li><li><p>How about if it is doing what it is intended to do, but only for half our users?What about 1% of our users?</p></li><li><p>Does a system have to perform flawlessly to be considered &#8220;working&#8221;?</p></li><li><p>If the system is doing what it is supposed to, but delayed, is it &#8220;working&#8221;?</p></li></ul><p>Based on this ambiguity here, it is clear that from a design perspective, the meaning of what it means to be &#8220;working&#8221; <strong>relies on us to define</strong>. The acceptable limits of which errors are tolerated, the threshold, is also determined by us as the operators of the system.</p><p>Following this train thought, it means <strong>&#8220;working&#8221; is an arbitrary definition.</strong></p><h3>What are we building?</h3><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xzlv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F539d4c6d-3a8c-420c-b5ae-297880616355_800x332.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xzlv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F539d4c6d-3a8c-420c-b5ae-297880616355_800x332.png 424w, https://substackcdn.com/image/fetch/$s_!xzlv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F539d4c6d-3a8c-420c-b5ae-297880616355_800x332.png 848w, https://substackcdn.com/image/fetch/$s_!xzlv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F539d4c6d-3a8c-420c-b5ae-297880616355_800x332.png 1272w, https://substackcdn.com/image/fetch/$s_!xzlv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F539d4c6d-3a8c-420c-b5ae-297880616355_800x332.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xzlv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F539d4c6d-3a8c-420c-b5ae-297880616355_800x332.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/539d4c6d-3a8c-420c-b5ae-297880616355_800x332.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xzlv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F539d4c6d-3a8c-420c-b5ae-297880616355_800x332.png 424w, https://substackcdn.com/image/fetch/$s_!xzlv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F539d4c6d-3a8c-420c-b5ae-297880616355_800x332.png 848w, https://substackcdn.com/image/fetch/$s_!xzlv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F539d4c6d-3a8c-420c-b5ae-297880616355_800x332.png 1272w, https://substackcdn.com/image/fetch/$s_!xzlv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F539d4c6d-3a8c-420c-b5ae-297880616355_800x332.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">A map of various monitoring system components</figcaption></figure></div><p>If we refine the answers to our conceptual questions above, we are left with the essence of monitoring: we are building something that can tell whether another system is functioning based on the parameters we define.</p><p>In essence&#8202;&#8212;&#8202;observing. There are, of course, other elements of monitoring such as alerting, recovering, etc.&#8202;&#8212;&#8202;we&#8217;ll get to that later.</p><h3>The basic parts of a monitoring system</h3><h4>The Monitor</h4><p>The Monitor is the component that contains the general logic for monitoring. Note that it doesn&#8217;t contain the logic for monitoring a specific system, but rather the algorithm for monitoring:</p><ul><li><p>Step 1: Check a system and get a result</p></li><li><p>Step 2a: If the result is &#8220;working&#8221;, do something</p></li><li><p>Step 2b: If the result is &#8220;not working&#8221;, do something</p></li><li><p>Step 3: Repeat step 1</p></li></ul><div class="paywall-jump" data-component-name="PaywallToDOM"></div><p>If we break that down into a single section of code, we&#8217;ll get the following:</p><pre><code>loop
  result = check_system</code></pre><pre><code>  if result.working?
    puts 'system up'
  else
    puts 'system down'
  end
end</code></pre><p>Obviously, some details here are missing&#8202;&#8212;&#8202;we&#8217;ll get to this.</p><h4>The Configuration</h4><p>Remember&#8202;&#8212;&#8202;one of our key constraints is the fact that the system being monitored is arbitrary. This means that the Monitor doesn&#8217;t actually know how to talk to or check the system.</p><p>The configuration defines what the Monitor actually does. This is a piece of data, articulated as code or stored in a database.</p><pre><code>configuration = {
  frequency: "2.days",
  target: "PaymentVendor"
}</code></pre><p>One could easily write code to interpret this:</p><pre><code>def is_time_to_check_system?(frequency)
  interval_number = frequency.split('.').first
  interval_unit = frequency.split('.').second</code></pre><pre><code>  interval = if interval_unit == 'days'
    interval_number * (60 * 24)  
  elsif interval_unit == 'minutes'
    interval_number * 60
  end</code></pre><pre><code>  return last_observed_at + interval &lt; Time.now
end</code></pre><p>The resulting algorithm:</p><pre><code>loop
  next unless is_time_to_check_system?(configuration[:frequency])</code></pre><pre><code>  result = check_system</code></pre><pre><code>  if result.working?
    puts 'system up'
  else
    puts 'system down'
  end
end</code></pre><p>Where the configuration lives is not particularly relevant to the monitoring system&#8202;&#8212;&#8202;it could be in the code, in a file, or even in the database if you want users to be able to set up these configurations themselves or want them defined easily during runtime!</p><h4><strong>The Integration</strong></h4><p>This is the part of the system that actually knows how to contact the thing being monitored. You&#8217;ll want to make sure it has a consistent calling interface for the monitor to use, to allow for multiple kinds of integrations to be monitored.</p><p>Let&#8217;s suppose you&#8217;re monitoring a 3rd-party payment system that has an API that always returns a response with a status code:</p><ul><li><p><code>SUCCESS</code> if the request succeeded</p></li><li><p><code>FAILED</code> if the request failed</p></li></ul><p>You can create a library that calls this API endpoint and interprets the response into a standard format your monitoring system can understand.</p><pre><code>class PaymentVendorMonitorIntegration &lt; MonitorIntegration
  def call(params = {})
    vendor_response = VendorLib.http_call(params)
    monitor_result = to_monitoring_system_result(vendor_response)
    
    return monitor_result
  end</code></pre><pre><code>  def to_monitoring_system_result(vendor_response)
    return {
      status: vendor_response.status == 'success' ? 'UP' : 'DOWN',
      message: vendor_response.body['message']
    }
  end
end</code></pre><p>Here, the standard interface for a MonitorIntegration is <code>#call</code> and the result which contains a <code>status</code> and a <code>message</code>&nbsp;. While an actual implementation will contain many more things, this fundamentally is the core of determining a system&#8217;s status.</p><pre><code>def check_system(target)
  monitor_class = "#{target}MonitorIntegration".constantize
  monitor_class.new.call
end</code></pre><p>You could the integrate with the above code:</p><pre><code>loop
  next unless is_time_to_check_system?(configuration[:frequency])</code></pre><pre><code>  result = check_system(configuration[:target])</code></pre><pre><code>  if result.working?
    puts 'system up'
  else
    puts 'system down'
  end
end</code></pre><pre><code>is_time_to_check_system?(frequency) # ...
check_system(target) # ...</code></pre><h4>The Standardized Response</h4><p>The important part of monitoring multiple systems is you want to standardize the response regardless of what that system gives you.</p><p>You could map the <code>to_monitoring_system_result</code> into a class:</p><pre><code>class MonitorResult</code></pre><pre><code>  def initialize(params = {})
    @params = params
  end</code></pre><pre><code>  def working?
    @params[:status] == 'up'
  end</code></pre><pre><code>end</code></pre><p>Now, when you receive a result from a monitor, you can expect to be able to call <code>working?</code> and have it return a consistent result.</p><h3><strong>That&#8217;s the basics of observing!</strong></h3><p>Obviously, your code will have to change to support different integration targets using some sort of system identifier.</p><p>There&#8217;s multiple iterations available from here:</p><ul><li><p>Supporting more advanced system checks, such as running more complex scripts</p></li><li><p>Supporting specificity of endpoints, or adding parameters to checks</p></li><li><p>Moving configurations into the database to allow for user-configured checks</p></li></ul><p>These I leave as an exercise to the reader.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!FPxj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f49165c-042b-4a95-8efa-7e23ef7f2368_800x347.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!FPxj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f49165c-042b-4a95-8efa-7e23ef7f2368_800x347.png 424w, https://substackcdn.com/image/fetch/$s_!FPxj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f49165c-042b-4a95-8efa-7e23ef7f2368_800x347.png 848w, https://substackcdn.com/image/fetch/$s_!FPxj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f49165c-042b-4a95-8efa-7e23ef7f2368_800x347.png 1272w, https://substackcdn.com/image/fetch/$s_!FPxj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f49165c-042b-4a95-8efa-7e23ef7f2368_800x347.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!FPxj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f49165c-042b-4a95-8efa-7e23ef7f2368_800x347.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8f49165c-042b-4a95-8efa-7e23ef7f2368_800x347.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!FPxj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f49165c-042b-4a95-8efa-7e23ef7f2368_800x347.png 424w, https://substackcdn.com/image/fetch/$s_!FPxj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f49165c-042b-4a95-8efa-7e23ef7f2368_800x347.png 848w, https://substackcdn.com/image/fetch/$s_!FPxj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f49165c-042b-4a95-8efa-7e23ef7f2368_800x347.png 1272w, https://substackcdn.com/image/fetch/$s_!FPxj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f49165c-042b-4a95-8efa-7e23ef7f2368_800x347.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h3>What do you want to do after you monitor a&nbsp;system?</h3><p>If a tree falls in the forest and no living thing is around to hear it, does it make it sound? Does it matter if it did?</p><p>There&#8217;s no point in monitoring something if you don&#8217;t have an action item arising out of it. What we do after we determine whether something is working or not working matters just as much as determining it in the first place.</p><p>Our monitor above just logs to the console&#8202;&#8212;&#8202;that&#8217;s not useful. You&#8217;ll want to add additional behaviors beyond just observing. These may include:</p><ul><li><p><strong>Alerting</strong>&#8202;&#8212;&#8202;Letting people know when the system is operating outside of defined norms.</p></li><li><p><strong>Tracking&#8202;</strong>&#8212;&#8202;Recording statuses over time so that trends and long-term empirical baselines can be established</p></li><li><p><strong>Recovering&#8202;</strong>&#8212;&#8202;Triggering automatic actions that can help the system recover.</p></li></ul><p>We&#8217;ll explore an approach for each of these.</p><h3>Alerting</h3><p>Suppose you want to send a Slack message if a critical system is down. You an do something like:</p><pre><code>def send_incident_notification
  SlackNotification.send_to('#outages', 'System is down!')
end</code></pre><p>Which could go here:</p><pre><code>loop
  next unless is_time_to_check_system?(configuration[:frequency])</code></pre><pre><code>  result = check_system(configuration[:target])</code></pre><pre><code>  if result.working?
    puts 'system up'
  else
    send_incident_notification
  end
end</code></pre><h3>Tracking</h3><p>You&#8217;ll likely want to have a history of whether a system is down or not so you can report and track uptime over a larger period of time.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HwUM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c3a6a4a-1312-4ba1-92e1-c4480f7dbc9b_800x107.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HwUM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c3a6a4a-1312-4ba1-92e1-c4480f7dbc9b_800x107.png 424w, https://substackcdn.com/image/fetch/$s_!HwUM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c3a6a4a-1312-4ba1-92e1-c4480f7dbc9b_800x107.png 848w, https://substackcdn.com/image/fetch/$s_!HwUM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c3a6a4a-1312-4ba1-92e1-c4480f7dbc9b_800x107.png 1272w, https://substackcdn.com/image/fetch/$s_!HwUM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c3a6a4a-1312-4ba1-92e1-c4480f7dbc9b_800x107.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HwUM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c3a6a4a-1312-4ba1-92e1-c4480f7dbc9b_800x107.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6c3a6a4a-1312-4ba1-92e1-c4480f7dbc9b_800x107.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!HwUM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c3a6a4a-1312-4ba1-92e1-c4480f7dbc9b_800x107.png 424w, https://substackcdn.com/image/fetch/$s_!HwUM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c3a6a4a-1312-4ba1-92e1-c4480f7dbc9b_800x107.png 848w, https://substackcdn.com/image/fetch/$s_!HwUM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c3a6a4a-1312-4ba1-92e1-c4480f7dbc9b_800x107.png 1272w, https://substackcdn.com/image/fetch/$s_!HwUM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c3a6a4a-1312-4ba1-92e1-c4480f7dbc9b_800x107.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h4>The naive&nbsp;approach</h4><p>The first approach is simple: log the results of all in your favorite data store.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Vufb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1f15efa-9056-42f6-87ff-11e108db1742_337x125.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Vufb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1f15efa-9056-42f6-87ff-11e108db1742_337x125.png 424w, https://substackcdn.com/image/fetch/$s_!Vufb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1f15efa-9056-42f6-87ff-11e108db1742_337x125.png 848w, https://substackcdn.com/image/fetch/$s_!Vufb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1f15efa-9056-42f6-87ff-11e108db1742_337x125.png 1272w, https://substackcdn.com/image/fetch/$s_!Vufb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1f15efa-9056-42f6-87ff-11e108db1742_337x125.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Vufb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1f15efa-9056-42f6-87ff-11e108db1742_337x125.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c1f15efa-9056-42f6-87ff-11e108db1742_337x125.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Vufb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1f15efa-9056-42f6-87ff-11e108db1742_337x125.png 424w, https://substackcdn.com/image/fetch/$s_!Vufb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1f15efa-9056-42f6-87ff-11e108db1742_337x125.png 848w, https://substackcdn.com/image/fetch/$s_!Vufb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1f15efa-9056-42f6-87ff-11e108db1742_337x125.png 1272w, https://substackcdn.com/image/fetch/$s_!Vufb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1f15efa-9056-42f6-87ff-11e108db1742_337x125.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Store the status in your favorite data&nbsp;store</figcaption></figure></div><pre><code>def log_monitor_result(result)
  MonitorResult.create!(status: result.working?, 
                        occurred_at: Time.now)
end</code></pre><p>You can incorporate it easily after getting the result:</p><pre><code>loop
  next unless is_time_to_check_system?(configuration[:frequency])</code></pre><pre><code>  result = check_system(configuration[:target])</code></pre><pre><code>  log_monitor_result(result)</code></pre><pre><code>  if result.working?
    puts 'system up'
  else
    send_incident_notification
  end
end</code></pre><p>Once you have a history of statuses, you can easily create a chart that shows uptime and availability at whatever frequency you desire.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dO31!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45a68c8-501f-499b-9193-befce52d1b19_800x276.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dO31!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45a68c8-501f-499b-9193-befce52d1b19_800x276.png 424w, https://substackcdn.com/image/fetch/$s_!dO31!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45a68c8-501f-499b-9193-befce52d1b19_800x276.png 848w, https://substackcdn.com/image/fetch/$s_!dO31!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45a68c8-501f-499b-9193-befce52d1b19_800x276.png 1272w, https://substackcdn.com/image/fetch/$s_!dO31!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45a68c8-501f-499b-9193-befce52d1b19_800x276.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dO31!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45a68c8-501f-499b-9193-befce52d1b19_800x276.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f45a68c8-501f-499b-9193-befce52d1b19_800x276.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dO31!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45a68c8-501f-499b-9193-befce52d1b19_800x276.png 424w, https://substackcdn.com/image/fetch/$s_!dO31!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45a68c8-501f-499b-9193-befce52d1b19_800x276.png 848w, https://substackcdn.com/image/fetch/$s_!dO31!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45a68c8-501f-499b-9193-befce52d1b19_800x276.png 1272w, https://substackcdn.com/image/fetch/$s_!dO31!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45a68c8-501f-499b-9193-befce52d1b19_800x276.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Assigning a &#8220;0&#8221; or &#8220;1&#8221; to the status lets you chart downtimes.</figcaption></figure></div><h4>A smarter&nbsp;approach</h4><p>This naive approach has a key downsides&#8202;&#8212;&#8202;you&#8217;ll store a lot of records.</p><ul><li><p>If you&#8217;re monitoring just one system every minute, that&#8217;s 1,440 records / day, or 525,600 per year.</p></li><li><p>If you&#8217;re monitoring multiple things, such as 50 different systems, that&#8217;s 72,000 records / day or 26,280,000 per year.</p></li></ul><p>A better approach isn&#8217;t to log every attempt, but to <strong>log only if the status has changed.</strong></p><p>This would only log the things we actually care about &#8212;outages or recoveries, and allow us to assume the last known status for any time period afterwards.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lshR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc9be5bd-b652-49f9-89fd-9a2c5b14db4c_336x126.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lshR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc9be5bd-b652-49f9-89fd-9a2c5b14db4c_336x126.png 424w, https://substackcdn.com/image/fetch/$s_!lshR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc9be5bd-b652-49f9-89fd-9a2c5b14db4c_336x126.png 848w, https://substackcdn.com/image/fetch/$s_!lshR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc9be5bd-b652-49f9-89fd-9a2c5b14db4c_336x126.png 1272w, https://substackcdn.com/image/fetch/$s_!lshR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc9be5bd-b652-49f9-89fd-9a2c5b14db4c_336x126.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lshR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc9be5bd-b652-49f9-89fd-9a2c5b14db4c_336x126.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bc9be5bd-b652-49f9-89fd-9a2c5b14db4c_336x126.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lshR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc9be5bd-b652-49f9-89fd-9a2c5b14db4c_336x126.png 424w, https://substackcdn.com/image/fetch/$s_!lshR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc9be5bd-b652-49f9-89fd-9a2c5b14db4c_336x126.png 848w, https://substackcdn.com/image/fetch/$s_!lshR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc9be5bd-b652-49f9-89fd-9a2c5b14db4c_336x126.png 1272w, https://substackcdn.com/image/fetch/$s_!lshR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc9be5bd-b652-49f9-89fd-9a2c5b14db4c_336x126.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Logging only changes reduces the storage footprint considerably.</figcaption></figure></div><p>This has a best-case storage requirement of 1 database record if your system never goes down.</p><pre><code>def is_different_than_last_status?(result)
  previous_result = MonitorResult.all.order(:occurred_at).desc.first
  previous_result.try(:status) != result[:status]
end</code></pre><p>Our new algorithm looks like below:</p><pre><code>loop
  next unless is_time_to_check_system?(configuration[:frequency])</code></pre><pre><code>  result = check_system(configuration[:target])</code></pre><pre><code>  if is_different_than_last_status?(result)
    log_monitor_result(result)
  end</code></pre><pre><code>  if result.working?
    puts 'system up'
  else
    send_incident_notification
  end
end</code></pre><h4>Future iterations</h4><p>Tracking additional things can help you monitor and manage even more&#8202;&#8212;&#8202;you could easily track:</p><ul><li><p>Additional data like HTTP status codes</p></li><li><p>Response payloads to inform outage resolution</p></li><li><p>Response times for performance management</p></li></ul><p>and more!</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DVE-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce41a88c-960f-47ce-b46d-7c07bd11d4b8_520x182.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DVE-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce41a88c-960f-47ce-b46d-7c07bd11d4b8_520x182.png 424w, https://substackcdn.com/image/fetch/$s_!DVE-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce41a88c-960f-47ce-b46d-7c07bd11d4b8_520x182.png 848w, https://substackcdn.com/image/fetch/$s_!DVE-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce41a88c-960f-47ce-b46d-7c07bd11d4b8_520x182.png 1272w, https://substackcdn.com/image/fetch/$s_!DVE-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce41a88c-960f-47ce-b46d-7c07bd11d4b8_520x182.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DVE-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce41a88c-960f-47ce-b46d-7c07bd11d4b8_520x182.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ce41a88c-960f-47ce-b46d-7c07bd11d4b8_520x182.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!DVE-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce41a88c-960f-47ce-b46d-7c07bd11d4b8_520x182.png 424w, https://substackcdn.com/image/fetch/$s_!DVE-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce41a88c-960f-47ce-b46d-7c07bd11d4b8_520x182.png 848w, https://substackcdn.com/image/fetch/$s_!DVE-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce41a88c-960f-47ce-b46d-7c07bd11d4b8_520x182.png 1272w, https://substackcdn.com/image/fetch/$s_!DVE-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce41a88c-960f-47ce-b46d-7c07bd11d4b8_520x182.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The limits of tracking are what you decide to&nbsp;store</figcaption></figure></div><h3>Recovering</h3><p>If you have a reliable way to make the system function properly, you can execute this in the event of downtime fairly easily, creating self-recovering systems.</p><pre><code>def attempt_system_recovery
  send_restart_command # I leave this to your imagination
end</code></pre><p>Incorporating it when your system is seen as down can help automate recoveries.</p><pre><code>loop
  next unless is_time_to_check_system?(configuration[:frequency])</code></pre><pre><code>result = check_system(configuration[:target])</code></pre><pre><code>if is_different_than_last_status?(result)
    log_monitor_result(result)
  end</code></pre><pre><code>if result.working?
    puts 'system up'
  else
    send_incident_notification
    attempt_system_recovery
  end
end</code></pre><p>This is, of course, a proof of concept, and not something you should use in production&#8202;&#8212;&#8202;unconstrained automated system recoveries can easily cause your system to never recover or do things that makes the incident even worse. Be judicious!</p><h3>A final generalization</h3><p>What do Alerting, Tracking, and Recovering all have in common? That&#8217;s right&#8202;&#8212;&#8202;their lifecycle is tied to the monitoring system.</p><p>When a monitor is triggered, and a result is received, these are ultimately actions that occur. One could take the system a step further and generalize it into a MonitorAction.</p><pre><code>class MonitorAction
  def do(params = {})
  end
  
  def should_do?(config, result)
  end
end</code></pre><p>Implementations of Alerting, Tracking, and Recovering could look like:</p><pre><code>class AlertMonitorAction &lt; MonitorAction
  def do(params = {})
    SlackNotification.send_to('#outages', 'System is down!')
  end</code></pre><pre><code>  def should_do?(config, result)
    result.working?
  end
end</code></pre><pre><code>class TrackMonitorAction &lt; MonitorAction
  def do(params = {})
    MonitorResult.create!(status: result.working?, 
                          occurred_at: Time.now)
  end</code></pre><pre><code>  def should_do?(config, result)
    previous_result = MonitorResult.order(:occurred_at).desc.first
    previous_result.try(:status) != result[:status]
  end
end</code></pre><pre><code>class RecoverMonitorAction &lt; MonitorAction
  def do(params = {})
    send_restart_command
  end
end</code></pre><p>The algorithm could call a <code>perform_actions</code> function that checks whether something should happen:</p><pre><code>def perform_actions(configuration, result)
  configuration[:actions].each{ |action_name| 
    action = "#{action_name}Action".constantize.new
    if action.should_do?
      action.do(configuration, result)
    end
  }
end</code></pre><p>The algorithm would be changed to:</p><pre><code>configuration = {
  frequency: "2.days",
  target: "PaymentVendor",
  actions: ['Monitor', 'Track'] # whatever you want to run
}

</code></pre><pre><code>loop
  next unless is_time_to_check_system?(configuration[:frequency])</code></pre><pre><code>  result = check_system(configuration[:target])
  
  perform_actions(configuration, result)
end</code></pre><p>You can see how this could evolve even further to support different actions, parameterized actions per configuration, or even actions before the system is checked.</p><p>You could even move the configuration into the database so that it could be defined by the user.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yD94!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae86688d-2938-4875-9364-0deadf04edb9_784x589.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yD94!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae86688d-2938-4875-9364-0deadf04edb9_784x589.png 424w, https://substackcdn.com/image/fetch/$s_!yD94!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae86688d-2938-4875-9364-0deadf04edb9_784x589.png 848w, https://substackcdn.com/image/fetch/$s_!yD94!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae86688d-2938-4875-9364-0deadf04edb9_784x589.png 1272w, https://substackcdn.com/image/fetch/$s_!yD94!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae86688d-2938-4875-9364-0deadf04edb9_784x589.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yD94!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae86688d-2938-4875-9364-0deadf04edb9_784x589.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ae86688d-2938-4875-9364-0deadf04edb9_784x589.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yD94!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae86688d-2938-4875-9364-0deadf04edb9_784x589.png 424w, https://substackcdn.com/image/fetch/$s_!yD94!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae86688d-2938-4875-9364-0deadf04edb9_784x589.png 848w, https://substackcdn.com/image/fetch/$s_!yD94!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae86688d-2938-4875-9364-0deadf04edb9_784x589.png 1272w, https://substackcdn.com/image/fetch/$s_!yD94!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae86688d-2938-4875-9364-0deadf04edb9_784x589.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Actions can be generalized and moved into the database for per-configuration behavior.</figcaption></figure></div><h3>That&#8217;s it!</h3><p>Monitoring systems are conceptually fairly simple.</p><p>However, a real production system is so much more: production monitors are backgrounded, off of the main application, and have some sort of user-defined interface to set up. There could be even more specificity like monitoring endpoints, status interpretation beyond a binary &#8220;up&#8221; / &#8220;down&#8221;, etc.</p><p>Real-life complexity comes from ensuring it functions reliably and can perform reliable recovery if needed, and can track to the granularity needed.</p><p>Did you like this article? Let me know in the comments, or connect with me on <a href="https://www.linkedin.com/in/jgefroh/">LinkedIn</a>!</p>]]></content:encoded></item><item><title><![CDATA[How to Design Software — Report Generators]]></title><description><![CDATA[Learn how to create a reusable report generation system using the template design pattern]]></description><link>https://blog.jgefroh.com/p/how-to-design-software-report-generators-in-ruby-6691df98a518</link><guid isPermaLink="false">https://blog.jgefroh.com/p/how-to-design-software-report-generators-in-ruby-6691df98a518</guid><dc:creator><![CDATA[Joseph Gefroh]]></dc:creator><pubDate>Mon, 09 Aug 2021 15:39:36 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/e972c981-e238-4117-918c-4374c1df4b1a_800x418.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1SSQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9faa295d-2502-4488-91cf-d91f91277ce8_800x418.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1SSQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9faa295d-2502-4488-91cf-d91f91277ce8_800x418.png 424w, https://substackcdn.com/image/fetch/$s_!1SSQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9faa295d-2502-4488-91cf-d91f91277ce8_800x418.png 848w, https://substackcdn.com/image/fetch/$s_!1SSQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9faa295d-2502-4488-91cf-d91f91277ce8_800x418.png 1272w, https://substackcdn.com/image/fetch/$s_!1SSQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9faa295d-2502-4488-91cf-d91f91277ce8_800x418.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1SSQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9faa295d-2502-4488-91cf-d91f91277ce8_800x418.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9faa295d-2502-4488-91cf-d91f91277ce8_800x418.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!1SSQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9faa295d-2502-4488-91cf-d91f91277ce8_800x418.png 424w, https://substackcdn.com/image/fetch/$s_!1SSQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9faa295d-2502-4488-91cf-d91f91277ce8_800x418.png 848w, https://substackcdn.com/image/fetch/$s_!1SSQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9faa295d-2502-4488-91cf-d91f91277ce8_800x418.png 1272w, https://substackcdn.com/image/fetch/$s_!1SSQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9faa295d-2502-4488-91cf-d91f91277ce8_800x418.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><h4>Learn how to create a reusable export generation system using the template design&nbsp;pattern.</h4><p>Reports. It&#8217;s often one of the first features developers are asked to build by the business. Once someone has data, what do they want to do with it?</p><p>That&#8217;s right&#8202;&#8212;&#8202;view it.</p><h4>Engineers have problems with&nbsp;reports</h4><p>Many engineers approach reports in a use-case particular way.</p><p>They&#8217;ll get a requirement such as <em>&#8220;create a&nbsp;.csv report that displays all purchased items from the store&#8221;</em> and they&#8217;ll go and create that particular feature.</p><p>The next time they may get a request to <em>&#8220;create a&nbsp;.csv report of all users belonging to the account&#8221; </em>and they&#8217;ll go and create that feature as well.</p><p>As time goes on, the business asks for different formats, different fields, etc. These requests add up, and after a few iterations, the system ends up with more than a dozen report implementations, all with their own bugs and quirks.</p><p>Worse yet, if any major iterations are needed, such as customizability or some new performance improvement, the change will have to be made over and over (and likely slightly differently each time), leading to significant overhead implementation and maintenance costs just to generate a comma-separated list! But, it doesn&#8217;t have to be this way. There are some simple techniques you can use to avoid this pain altogether.</p><h3>What Is a&nbsp;report?</h3><p>First, let&#8217;s understand&#8202;&#8212;&#8202;what is a report, exactly? Every report has these four basic components:</p><ul><li><p>data records</p></li><li><p>data values</p></li><li><p>labels</p></li><li><p>format</p></li></ul><h4>Data records</h4><p>The data is the actual data that you are using to populate your report. Maybe it is a bunch of events in your system. Perhaps it is transaction data. It&#8217;s likely data pulled from your database.</p><p>There&#8217;s likely a specific scope to the data to limit the data based on some parameter, such as date range.</p><h4>Data values</h4><p>It&#8217;s not enough to have just the data as a whole. People are often interested in specific data points&#8202;&#8212;&#8202;specific fields of the data. A financial person may only be interested in dollar amounts of a settlement, whereas a security monitor may be interested in an address mismatch status.</p><h4>Labels</h4><p>Labels provide meaning to the data. It&#8217;s what distinguishes this&#8230;:</p><pre><code>5 | 3 | 230 | 40</code></pre><p>&#8230;from this:</p><pre><code>id | event_id | amount_paid_cents | fee_paid_cents
5  | 3        | 230               | 40</code></pre><p>Labels turn meaningless values into data and provide human-understandable context to otherwise arbitrary values.</p><h4>Format</h4><p>Finally, there&#8217;s the format of the report. The format could be arbitrary and what format is needed ultimately depends on the consumer. If the consumer is an API, the format might be JSON.</p><p>If the consumer is a FTP drop-off or a person intending to import it into another system, it may be a CSV. If the consumer is a data person interested in slicing and dicing, it may end up as an Excel spreadsheet or even a set of database import commands.</p><h3>What Are the Steps of Generating a&nbsp;Report?</h3><p>Now that we know the pieces, we can look at the algorithm: what is the sequence of steps needed to generate <em>any</em><strong> </strong>kind of report?</p><p>Turns out, it&#8217;s not a complicated algorithm! It can be encapsulated into the following:</p><ul><li><p>Setup&#8202;&#8212;&#8202;Get parameters for the report</p></li><li><p>Fetching records</p></li><li><p>Map&#8202;&#8212;&#8202;Get a particular set of fields from each record</p></li><li><p>Convert each set of fields into an entry in the report</p></li><li><p>Send the report back to the user</p></li></ul><h4>Setup</h4><p>The first step is setup&#8202;&#8212;&#8202;sending the parameters into the report to adjust how it behaviors. This is often data used for <em>scoping<strong>&#8202;&#8212;&#8202;</strong></em>some collection of records needs to be filtered and reduced into a subset (eg. by an account, by date, etc.)</p><h4>Fetch</h4><p>Once you have the parameters for your report, you&#8217;ll want to start retrieving those records. You can apply the filters as appropriate:</p><div class="paywall-jump" data-component-name="PaywallToDOM"></div><h4>Map</h4><p>You&#8217;re likely interested in a particular set of attributes and fields in the record. You can turn your records into that particular set, and optionally enrich each entry with additional data.</p><h4>Convert</h4><p>Once you have your collection&#8202;&#8212;&#8202;in this case, a set of hashes, you&#8217;ll want to convert it into a specific format, such as CSV.</p><p>Want to support other formats? No problem:</p><h3>Putting It All&nbsp;Together</h3><p>Each of the steps above may have its own details, but you can generally encapsulate it into something like below:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eN5_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d39f861-d8c4-4ac0-8c89-ea507b5ada27_800x285.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eN5_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d39f861-d8c4-4ac0-8c89-ea507b5ada27_800x285.png 424w, https://substackcdn.com/image/fetch/$s_!eN5_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d39f861-d8c4-4ac0-8c89-ea507b5ada27_800x285.png 848w, https://substackcdn.com/image/fetch/$s_!eN5_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d39f861-d8c4-4ac0-8c89-ea507b5ada27_800x285.png 1272w, https://substackcdn.com/image/fetch/$s_!eN5_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d39f861-d8c4-4ac0-8c89-ea507b5ada27_800x285.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eN5_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d39f861-d8c4-4ac0-8c89-ea507b5ada27_800x285.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3d39f861-d8c4-4ac0-8c89-ea507b5ada27_800x285.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!eN5_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d39f861-d8c4-4ac0-8c89-ea507b5ada27_800x285.png 424w, https://substackcdn.com/image/fetch/$s_!eN5_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d39f861-d8c4-4ac0-8c89-ea507b5ada27_800x285.png 848w, https://substackcdn.com/image/fetch/$s_!eN5_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d39f861-d8c4-4ac0-8c89-ea507b5ada27_800x285.png 1272w, https://substackcdn.com/image/fetch/$s_!eN5_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d39f861-d8c4-4ac0-8c89-ea507b5ada27_800x285.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>A use-case specific implementation might look like:</p><p>Note how easy it is to tie into the existing functionality of a report&#8202;&#8212;&#8202;you can quickly create many different kinds of reports without worrying about how to generate or deliver them.</p><p>If it looks familiar&#8202;&#8212;&#8202;it&#8217;s because it might just be!</p><p>This is the template pattern<strong>&#8202;</strong>&#8212;&#8202;a design pattern that helps encapsulate the sequence of an algorithm and the implementation of the invariant steps while leaving the variant steps available to be &#8220;filled in&#8221; by a subclass.</p><h3>Additional Iterations</h3><p>There&#8217;s a lot of other concerns and possible iterations a report might have, such as:</p><ul><li><p>Performance&#8202;&#8212;&#8202;record batching, record size management</p></li><li><p>Customization&#8202;&#8212;&#8202;specifying headers, changing ordering</p></li><li><p>Delivery&#8202;&#8212;&#8202;SFTP, download, email</p></li></ul><p>This functionality can be added to the base class or subclasses as appropriate without changing the overall structure of the system. Standardization of inputs and outputs allows for tie-ins into other parts of your system.</p><p>For example, if you already have a module that delivers files via email, you can easily plug in the output of your report generator into that subsystem, or call it from a function like <code>deliver_to_email(email)</code>.</p><p>That&#8217;s it! Creating reusable software that separates the mechanism from the use case, allowing it to be used in many other use cases, is as simple as breaking it down into small parts and thinking carefully about inputs and outputs.</p><p>Did you like this article? Let me know in the comments, or connect with me on <a href="https://www.linkedin.com/in/jgefroh/">LinkedIn</a>!</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9wmN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e6300f0-8580-4f29-a016-ddef6fb347c1_250x250.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9wmN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e6300f0-8580-4f29-a016-ddef6fb347c1_250x250.png 424w, https://substackcdn.com/image/fetch/$s_!9wmN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e6300f0-8580-4f29-a016-ddef6fb347c1_250x250.png 848w, https://substackcdn.com/image/fetch/$s_!9wmN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e6300f0-8580-4f29-a016-ddef6fb347c1_250x250.png 1272w, https://substackcdn.com/image/fetch/$s_!9wmN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e6300f0-8580-4f29-a016-ddef6fb347c1_250x250.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9wmN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e6300f0-8580-4f29-a016-ddef6fb347c1_250x250.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2e6300f0-8580-4f29-a016-ddef6fb347c1_250x250.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9wmN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e6300f0-8580-4f29-a016-ddef6fb347c1_250x250.png 424w, https://substackcdn.com/image/fetch/$s_!9wmN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e6300f0-8580-4f29-a016-ddef6fb347c1_250x250.png 848w, https://substackcdn.com/image/fetch/$s_!9wmN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e6300f0-8580-4f29-a016-ddef6fb347c1_250x250.png 1272w, https://substackcdn.com/image/fetch/$s_!9wmN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e6300f0-8580-4f29-a016-ddef6fb347c1_250x250.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div>]]></content:encoded></item><item><title><![CDATA[How to Design Software — Plugin Systems]]></title><description><![CDATA[Learn how I made a chatbot extensible by designing and building a plugin system to make it modular.]]></description><link>https://blog.jgefroh.com/p/how-to-design-software-plugins-d051ce1099b2</link><guid isPermaLink="false">https://blog.jgefroh.com/p/how-to-design-software-plugins-d051ce1099b2</guid><dc:creator><![CDATA[Joseph Gefroh]]></dc:creator><pubDate>Sun, 31 Jan 2021 01:15:16 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/71784283-a77f-4383-a4f4-f9d397bb4af3_800x418.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h4>Learn how to build a plugin system to allow others to extend your program&#8217;s functionality and modularity, using a chatbot as an&nbsp;example.</h4><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AFp2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a3e8905-e79e-4bd9-bf3b-ac298533c81e_800x418.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AFp2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a3e8905-e79e-4bd9-bf3b-ac298533c81e_800x418.png 424w, https://substackcdn.com/image/fetch/$s_!AFp2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a3e8905-e79e-4bd9-bf3b-ac298533c81e_800x418.png 848w, https://substackcdn.com/image/fetch/$s_!AFp2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a3e8905-e79e-4bd9-bf3b-ac298533c81e_800x418.png 1272w, https://substackcdn.com/image/fetch/$s_!AFp2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a3e8905-e79e-4bd9-bf3b-ac298533c81e_800x418.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AFp2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a3e8905-e79e-4bd9-bf3b-ac298533c81e_800x418.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7a3e8905-e79e-4bd9-bf3b-ac298533c81e_800x418.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!AFp2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a3e8905-e79e-4bd9-bf3b-ac298533c81e_800x418.png 424w, https://substackcdn.com/image/fetch/$s_!AFp2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a3e8905-e79e-4bd9-bf3b-ac298533c81e_800x418.png 848w, https://substackcdn.com/image/fetch/$s_!AFp2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a3e8905-e79e-4bd9-bf3b-ac298533c81e_800x418.png 1272w, https://substackcdn.com/image/fetch/$s_!AFp2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a3e8905-e79e-4bd9-bf3b-ac298533c81e_800x418.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>When I was younger, I had a top-of-the-line gaming computer. I spent thousands of hours playing games like Team Fortress 2, Minecraft, Guild Wars, and Age of Empires.</p><p>One of my favorite activities after I had thoroughly explored a game was the practice of <em>modding</em>. Modding allowed you to create or download packages of software that changed or added to how the software behaved&#8202;&#8212;&#8202;new levels, textures, or even game mechanics! The possibilities were limitless.</p><p>Now that I&#8217;m a software developer, I have a better understanding of how many of these programs supported such massive extensibility.</p><p>They used a concept called <em>plugins</em>.</p><h3>Plugin systems</h3><p>Plugins allow you to write subprograms that then hook into or are attached to a larger program. These subprograms then run, modifying or adding to the behavior of the running program.</p><p>In order to write a plugin, the program itself has to be written (or hacked) to support the plugin. Once this capability exists, you can &#8220;plug and play&#8221; tremendous amounts of functionality.</p><h3>The Concepts</h3><p>Plugin systems come in many shapes and forms&#8202;&#8212;&#8202;to illustrate the design, the following basic concepts can help frame your thinking:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!57NC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05b7066a-234d-45ba-8e54-de854afd92e7_800x498.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!57NC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05b7066a-234d-45ba-8e54-de854afd92e7_800x498.png 424w, https://substackcdn.com/image/fetch/$s_!57NC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05b7066a-234d-45ba-8e54-de854afd92e7_800x498.png 848w, https://substackcdn.com/image/fetch/$s_!57NC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05b7066a-234d-45ba-8e54-de854afd92e7_800x498.png 1272w, https://substackcdn.com/image/fetch/$s_!57NC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05b7066a-234d-45ba-8e54-de854afd92e7_800x498.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!57NC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05b7066a-234d-45ba-8e54-de854afd92e7_800x498.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/05b7066a-234d-45ba-8e54-de854afd92e7_800x498.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Diagram showing the conceptual components of a basic plugin system&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Diagram showing the conceptual components of a basic plugin system" title="Diagram showing the conceptual components of a basic plugin system" srcset="https://substackcdn.com/image/fetch/$s_!57NC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05b7066a-234d-45ba-8e54-de854afd92e7_800x498.png 424w, https://substackcdn.com/image/fetch/$s_!57NC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05b7066a-234d-45ba-8e54-de854afd92e7_800x498.png 848w, https://substackcdn.com/image/fetch/$s_!57NC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05b7066a-234d-45ba-8e54-de854afd92e7_800x498.png 1272w, https://substackcdn.com/image/fetch/$s_!57NC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05b7066a-234d-45ba-8e54-de854afd92e7_800x498.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Conceptual components of a basic plugin&nbsp;system</figcaption></figure></div><h4>The program</h4><p>The first piece is the program itself, the thing whose behavior you are actually trying to enhance or modify. This might be a game like Skyrim or it might be a business application like Sketch. It might even be your own program!</p><p>Whatever it is, it offers some set of behaviors and capabilities that you want to change. More importantly, it does not know about the changes you want to make.</p><h4>The hook</h4><p>Something in the program you&#8217;re trying to change has to run the code it doesn&#8217;t know about. Otherwise, it would be very difficult to change the behavior.</p><div class="paywall-jump" data-component-name="PaywallToDOM"></div><p>This point at which the code is run is called the <em>hook</em>. Rails&#8217; model callbacks or Vue&#8217;s component lifecycle hooks are examples of hooks used out in the wild.</p><p>A simple example of a hook is below:</p><p>See how the <code>onSaveHook</code> has no behavior. In this example, it is expected that this behavior will be filled in by a class extending <code>RecordSaver</code> and implementing new behavior in the subclass.</p><h4>The plugins</h4><p>The plugins are the code that other people write and &#8220;plug in&#8221; to enhance the capabilities of the program.</p><p>You could potentially extend the <code>RecordSaver</code> class from the example above with functionality such as below:</p><pre><code>class LoggedRecordSaver &lt; RecordSaver
  def onSaveHook()
    puts 'Saving record.'
  end
end</code></pre><p>Now when <code>LoggedRecordSaver#save()</code> is called, it will print <code>Saving record.</code> after calling <code>#writeToDisk()</code> to the console. This behavioral enhancement was done by extending <code>RecordSaver</code>&#8202;&#8212;&#8202;a basic way to enhance a program&#8217;s behavior.</p><p>Note also that <code>RecordSaver</code> doesn&#8217;t know anything about <code>LoggedRecordSaver</code>: the coupling is unidirectional.</p><h4>The loader</h4><p>Having a plugin is useless if it never actually manages to run. Something needs to load the Plugin to start executing its code.</p><p>There are a lot of techniques available to actually make this happen&#8202;&#8212;&#8202;two methods are:</p><ul><li><p><strong>Plugin-driven, </strong>where the plugin knows how to access the program and self-registers.</p></li><li><p><strong>Program-driven</strong>, where the program knows how to find plugins and loads any it finds.</p></li></ul><p>In plugin-driven loading approaches, the program provides a method by which the plugin can register itself with the program. In program-driven loading approaches, the program finds a plugin, such as by loading all files in a folder with the name <code>*_plugin</code> or by loading a manifest.</p><h3>A Chatbot Written Using&nbsp;Plugins</h3><p>A while ago, I spent a weekend writing a chatbot, OneLine, to tell me puns and send me reminders using natural language.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!77u9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e590ab-d847-4254-83d0-843ac9d7bef2_400x521.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!77u9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e590ab-d847-4254-83d0-843ac9d7bef2_400x521.png 424w, https://substackcdn.com/image/fetch/$s_!77u9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e590ab-d847-4254-83d0-843ac9d7bef2_400x521.png 848w, https://substackcdn.com/image/fetch/$s_!77u9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e590ab-d847-4254-83d0-843ac9d7bef2_400x521.png 1272w, https://substackcdn.com/image/fetch/$s_!77u9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e590ab-d847-4254-83d0-843ac9d7bef2_400x521.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!77u9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e590ab-d847-4254-83d0-843ac9d7bef2_400x521.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d3e590ab-d847-4254-83d0-843ac9d7bef2_400x521.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;example of author&#8217;s chatbot&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="example of author&#8217;s chatbot" title="example of author&#8217;s chatbot" srcset="https://substackcdn.com/image/fetch/$s_!77u9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e590ab-d847-4254-83d0-843ac9d7bef2_400x521.png 424w, https://substackcdn.com/image/fetch/$s_!77u9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e590ab-d847-4254-83d0-843ac9d7bef2_400x521.png 848w, https://substackcdn.com/image/fetch/$s_!77u9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e590ab-d847-4254-83d0-843ac9d7bef2_400x521.png 1272w, https://substackcdn.com/image/fetch/$s_!77u9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e590ab-d847-4254-83d0-843ac9d7bef2_400x521.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">OneLine, a chatbot I wrote that used a plugin&nbsp;approach</figcaption></figure></div><p>The approach was simple: receive a message, do something arbitrary with it, and send back a response. To build all this, I build a basic plugin system that implemented the concepts mentioned here.</p><h4>The loader</h4><p>OneLine had a part of the program, a subset of static methods on the <code>Core::Plugin</code> module, define an implementation to:</p><ul><li><p>Load a <code>plugin</code> (<code>#load</code>)</p></li><li><p>Track all loaded <code>plugins</code> (<code>@@plugins</code>)</p></li><li><p>Call all loaded <code>plugins</code> (<code>#call_all</code>)</p></li></ul><p>Note that the loader and the hook are both combined in this example.</p><p><strong>The loader</strong> is the <code>load</code> function that <code>Plugin</code> implementations can call to make the program aware of it.</p><p><strong>The hook</strong> is very explicitly a <code>call_all</code> function that is called when the program receives a message.</p><h4>The plugin interface</h4><p>It also defined an expectation for plugin implementations&#8202;&#8212;&#8202;behaviors all <code>plugins</code> should support. In this case:</p><ul><li><p>A check to see whether the plugin should run or not (<code>#process?</code>)</p></li><li><p>A method that will be called to run the plugin (<code>#process</code>)</p></li><li><p>A standardized response the plugin was expected to return (<code>#to_response</code>)</p></li></ul><p>The <code>#process</code> function is called by the hook (upon receipt of the message).</p><h4>The plugin implementations</h4><p>Finally, there are the plugin implementations that parse and interpret the message and do something with it. I made plugins to:</p><ul><li><p>Tell me a joke (<code>lib/oneline/jokes</code>)</p></li><li><p>Keep track of my to-do list (<code>lib/oneline/scheduler</code>)</p></li><li><p>&#8230;and more!</p></li></ul><p>The simplest example, a plugin that tells you the current time, is below.</p><h3>That&#8217;s It!</h3><p>Pretty straightforward! Approaching things in this manner left room for extensibility by others, kept different parts of the code segmented and isolated, and allowed me to build and support extensions, such as supporting a web server or conversations via SMS.</p><p>While plugin systems aren&#8217;t appropriate for all scenarios, they can be highly useful in some cases.</p><h3>Full Source Code Repository</h3><p>Feel free to check out the full source code for the chatbot (specifically in the <code>lib/oneline</code> folder):</p><p><strong><a href="https://github.com/jgefroh/oneline">JGefroh/oneline</a></strong><a href="https://github.com/jgefroh/oneline"><br></a><em><a href="https://github.com/jgefroh/oneline">OneLine is a personal assistant chatbot intended to simplify my life. You&#8217;re a busy person with many important things&#8230;</a></em><a href="https://github.com/jgefroh/oneline">github.com</a></p><p>Did you like this article? Let me know in the comments, or connect with me on <a href="https://www.linkedin.com/in/jgefroh/">LinkedIn</a>!</p>]]></content:encoded></item><item><title><![CDATA[How to Design Software — Dynamic Heatmaps (Chloropeths)]]></title><description><![CDATA[Learn how to create a dynamic heatmap of the United States by yourself, without having to buy a license from a company that sells charts.]]></description><link>https://blog.jgefroh.com/p/how-to-design-software-dynamic-heatmaps-e864742941b0</link><guid isPermaLink="false">https://blog.jgefroh.com/p/how-to-design-software-dynamic-heatmaps-e864742941b0</guid><dc:creator><![CDATA[Joseph Gefroh]]></dc:creator><pubDate>Sun, 26 Jul 2020 18:04:16 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa079a461-fc8f-4d7b-8d86-95f2d11cb4b3_250x250.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xbP6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb321156b-5e39-4694-802b-42ffa673dc76_800x418.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xbP6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb321156b-5e39-4694-802b-42ffa673dc76_800x418.png 424w, https://substackcdn.com/image/fetch/$s_!xbP6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb321156b-5e39-4694-802b-42ffa673dc76_800x418.png 848w, https://substackcdn.com/image/fetch/$s_!xbP6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb321156b-5e39-4694-802b-42ffa673dc76_800x418.png 1272w, https://substackcdn.com/image/fetch/$s_!xbP6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb321156b-5e39-4694-802b-42ffa673dc76_800x418.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xbP6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb321156b-5e39-4694-802b-42ffa673dc76_800x418.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b321156b-5e39-4694-802b-42ffa673dc76_800x418.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xbP6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb321156b-5e39-4694-802b-42ffa673dc76_800x418.png 424w, https://substackcdn.com/image/fetch/$s_!xbP6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb321156b-5e39-4694-802b-42ffa673dc76_800x418.png 848w, https://substackcdn.com/image/fetch/$s_!xbP6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb321156b-5e39-4694-802b-42ffa673dc76_800x418.png 1272w, https://substackcdn.com/image/fetch/$s_!xbP6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb321156b-5e39-4694-802b-42ffa673dc76_800x418.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><h4>Learn how to create a dynamic heatmap of the United States by yourself, without having to buy a license from a company that sells&nbsp;charts.</h4><p>A strangely large number of companies I&#8217;ve worked with somehow inevitably get to the point where they want to build or integrate a heatmap of some kind.</p><p>In the past, I would reach for the nearest charting library like HighCharts. Why reinvent the wheel, and besides, their charting is cool!</p><p>Over time, as I felt the itch to incorporate heatmaps into my weekend projects, I kept bumping it off due to the price. I kept asking myself &#8220;is this too expensive for a personal project?&#8221;</p><p><strong>I don&#8217;t know about you, but for me the answer is yes. </strong>Some charting libraries charge a licensing fee of <em>$500 per developer</em>. This prices out almost every individual looking to use it for any &#8220;for-fun&#8221; usage. I lose far too much money buying SPY puts in the stock market to be able to afford spending $500 for a weekend project.</p><p>So, like all engineers, I spent half an hour and made it myself.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!unYl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F29640dc6-9a9a-4048-b2e5-3616fce84e4c_800x516.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!unYl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F29640dc6-9a9a-4048-b2e5-3616fce84e4c_800x516.png 424w, https://substackcdn.com/image/fetch/$s_!unYl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F29640dc6-9a9a-4048-b2e5-3616fce84e4c_800x516.png 848w, https://substackcdn.com/image/fetch/$s_!unYl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F29640dc6-9a9a-4048-b2e5-3616fce84e4c_800x516.png 1272w, https://substackcdn.com/image/fetch/$s_!unYl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F29640dc6-9a9a-4048-b2e5-3616fce84e4c_800x516.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!unYl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F29640dc6-9a9a-4048-b2e5-3616fce84e4c_800x516.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/29640dc6-9a9a-4048-b2e5-3616fce84e4c_800x516.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!unYl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F29640dc6-9a9a-4048-b2e5-3616fce84e4c_800x516.png 424w, https://substackcdn.com/image/fetch/$s_!unYl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F29640dc6-9a9a-4048-b2e5-3616fce84e4c_800x516.png 848w, https://substackcdn.com/image/fetch/$s_!unYl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F29640dc6-9a9a-4048-b2e5-3616fce84e4c_800x516.png 1272w, https://substackcdn.com/image/fetch/$s_!unYl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F29640dc6-9a9a-4048-b2e5-3616fce84e4c_800x516.png 1456w" sizes="100vw"></picture><div></div></div></a><figcaption class="image-caption">Heatmap of the United States showing states randomly bucketed along a&nbsp;scale.</figcaption></figure></div><h3>What is a&nbsp;heatmap?</h3><p>A heatmap essentially shows a map and colors in various aspects of it based on some sort of scale.</p><p>The &#8220;map&#8221; doesn&#8217;t necessarily have to be a map&#8202;&#8212;&#8202;it can be a periodic table or a bunch of circles. What really matters is that there are distinct areas that will be colored differently depending on what bucket that distinct area falls in to.</p><p>As I later learned, these are common called <em>chloropeths</em>.</p><h3>The components</h3><p>There&#8217;s three aspects of a heatmap as far as web applications are concerned.</p><ul><li><p>the map</p></li><li><p>the code</p></li><li><p>the data</p></li></ul><h3>The map</h3><p>First off, you need an SVG of a map that you can color. I took this one from the Wikimedia Commons:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!E8fL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dc3566f-2693-4cb6-bf70-aee6cc7fe04a_800x533.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!E8fL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dc3566f-2693-4cb6-bf70-aee6cc7fe04a_800x533.png 424w, https://substackcdn.com/image/fetch/$s_!E8fL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dc3566f-2693-4cb6-bf70-aee6cc7fe04a_800x533.png 848w, https://substackcdn.com/image/fetch/$s_!E8fL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dc3566f-2693-4cb6-bf70-aee6cc7fe04a_800x533.png 1272w, https://substackcdn.com/image/fetch/$s_!E8fL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dc3566f-2693-4cb6-bf70-aee6cc7fe04a_800x533.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!E8fL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dc3566f-2693-4cb6-bf70-aee6cc7fe04a_800x533.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2dc3566f-2693-4cb6-bf70-aee6cc7fe04a_800x533.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!E8fL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dc3566f-2693-4cb6-bf70-aee6cc7fe04a_800x533.png 424w, https://substackcdn.com/image/fetch/$s_!E8fL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dc3566f-2693-4cb6-bf70-aee6cc7fe04a_800x533.png 848w, https://substackcdn.com/image/fetch/$s_!E8fL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dc3566f-2693-4cb6-bf70-aee6cc7fe04a_800x533.png 1272w, https://substackcdn.com/image/fetch/$s_!E8fL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dc3566f-2693-4cb6-bf70-aee6cc7fe04a_800x533.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">This particular US map was a SVG available for from <a href="https://commons.wikimedia.org/wiki/File:Blank_US_Map_%28states_only%29.svg">Wikimedia</a>.</figcaption></figure></div><h4>Breaking down an&nbsp;SVG</h4><p>If you ever interacted with an SVG file, you may have noticed it behaves differently than other image files. Some sites don&#8217;t let you upload them, they maintain their quality when their size changes, and your browser seems to be the default file opener in a lot of cases.</p><p><strong>Well, that&#8217;s because they are different. </strong>SVGs actually consist of markup. If you inspect an SVG file, you&#8217;ll see all of the markup that makes up the SVG image.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nOjr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16921d08-8a9b-4954-9d0f-c04390295a21_800x341.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nOjr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16921d08-8a9b-4954-9d0f-c04390295a21_800x341.png 424w, https://substackcdn.com/image/fetch/$s_!nOjr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16921d08-8a9b-4954-9d0f-c04390295a21_800x341.png 848w, https://substackcdn.com/image/fetch/$s_!nOjr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16921d08-8a9b-4954-9d0f-c04390295a21_800x341.png 1272w, https://substackcdn.com/image/fetch/$s_!nOjr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16921d08-8a9b-4954-9d0f-c04390295a21_800x341.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nOjr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16921d08-8a9b-4954-9d0f-c04390295a21_800x341.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/16921d08-8a9b-4954-9d0f-c04390295a21_800x341.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!nOjr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16921d08-8a9b-4954-9d0f-c04390295a21_800x341.png 424w, https://substackcdn.com/image/fetch/$s_!nOjr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16921d08-8a9b-4954-9d0f-c04390295a21_800x341.png 848w, https://substackcdn.com/image/fetch/$s_!nOjr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16921d08-8a9b-4954-9d0f-c04390295a21_800x341.png 1272w, https://substackcdn.com/image/fetch/$s_!nOjr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16921d08-8a9b-4954-9d0f-c04390295a21_800x341.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The markup for the map above, with some CSS added to the &lt;path&gt; element for Hawaii, coloring it&nbsp;red.</figcaption></figure></div><p>All of that incomprehensible garbage is actually the data that then makes up the individual lines and shapes. This means that you can do a lot of things like interact with it using Javascript and CSS. It behaves like HTML, so you can add styles, ids, attributes, etc.</p><p>That&#8217;s where the true power is.</p><h4>Prepare your&nbsp;SVG</h4><p>So, let&#8217;s assume you found or created your SVG. If you create one, I don&#8217;t recommend coding it&#8202;&#8212;&#8202;use a drawing tool that will output an SVG.</p><p>You need to prepare your SVG by adding identifiers to all of the various parts you want to independently color.</p><p>The map above was particularly convenient&#8202;&#8212;&#8202;it was already categorized into states. For others, you may need to do it yourself.</p><p>Each of the individual <code>&lt;path&gt;</code> elements that make up a state had an ID attached to it with the ISO 3166 Alpha-2 code.</p><p>Hawaii, for example, has an id of <code>HI</code>. If your map is missing it, simply add the <code>id="HI"</code> to the <code>&lt;path&gt;</code> that makes up Hawaii.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!oVRV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb901f248-76fe-4708-8737-cde8834756d3_800x203.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!oVRV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb901f248-76fe-4708-8737-cde8834756d3_800x203.png 424w, https://substackcdn.com/image/fetch/$s_!oVRV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb901f248-76fe-4708-8737-cde8834756d3_800x203.png 848w, https://substackcdn.com/image/fetch/$s_!oVRV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb901f248-76fe-4708-8737-cde8834756d3_800x203.png 1272w, https://substackcdn.com/image/fetch/$s_!oVRV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb901f248-76fe-4708-8737-cde8834756d3_800x203.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!oVRV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb901f248-76fe-4708-8737-cde8834756d3_800x203.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b901f248-76fe-4708-8737-cde8834756d3_800x203.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!oVRV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb901f248-76fe-4708-8737-cde8834756d3_800x203.png 424w, https://substackcdn.com/image/fetch/$s_!oVRV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb901f248-76fe-4708-8737-cde8834756d3_800x203.png 848w, https://substackcdn.com/image/fetch/$s_!oVRV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb901f248-76fe-4708-8737-cde8834756d3_800x203.png 1272w, https://substackcdn.com/image/fetch/$s_!oVRV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb901f248-76fe-4708-8737-cde8834756d3_800x203.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">These IDs will be used as handles to manipulate it in the future using Javascript.</figcaption></figure></div><h3>The Code</h3><p>Now, let&#8217;s write the coloring function. It&#8217;ll be a function that takes an identifier and sets a color. We&#8217;ll flesh this out later once we structure the data appropriately&#8202;&#8212;&#8202;for now let&#8217;s stub it out and return a randomly selected color.</p><div class="paywall-jump" data-component-name="PaywallToDOM"></div><p>We don&#8217;t care about the color specifically or how it is chosen. What we are caring about is tying it together.</p><pre><code>// map.js
function setColorFor(state) {
  let color = Math.random() &gt; 0.5 ? 'red' : 'blue';</code></pre><pre><code>  let element = document.getElementById(state)
  if (element) {
    element.style.fill = color;
  }
}</code></pre><pre><code>setColorFor('HI');</code></pre><h4>Put them both in an HTML&nbsp;file</h4><p>Let&#8217;s go and put them together.</p><pre><code>&lt;!-- map.html --&gt;
&lt;html&gt;
  &lt;body&gt;
    &lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 930 585"&gt;
      &lt;!-- Put the rest of the SVG here... --&gt;
    &lt;/svg&gt;</code></pre><pre><code>    &lt;script type="text/javascript" src="map.js"&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre><p>You should see something like below:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NuJW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0afab942-c16d-443c-b84a-041ec05fdab1_800x521.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NuJW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0afab942-c16d-443c-b84a-041ec05fdab1_800x521.png 424w, https://substackcdn.com/image/fetch/$s_!NuJW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0afab942-c16d-443c-b84a-041ec05fdab1_800x521.png 848w, https://substackcdn.com/image/fetch/$s_!NuJW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0afab942-c16d-443c-b84a-041ec05fdab1_800x521.png 1272w, https://substackcdn.com/image/fetch/$s_!NuJW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0afab942-c16d-443c-b84a-041ec05fdab1_800x521.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NuJW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0afab942-c16d-443c-b84a-041ec05fdab1_800x521.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0afab942-c16d-443c-b84a-041ec05fdab1_800x521.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NuJW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0afab942-c16d-443c-b84a-041ec05fdab1_800x521.png 424w, https://substackcdn.com/image/fetch/$s_!NuJW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0afab942-c16d-443c-b84a-041ec05fdab1_800x521.png 848w, https://substackcdn.com/image/fetch/$s_!NuJW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0afab942-c16d-443c-b84a-041ec05fdab1_800x521.png 1272w, https://substackcdn.com/image/fetch/$s_!NuJW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0afab942-c16d-443c-b84a-041ec05fdab1_800x521.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Open map.html using your browser and you should see Hawaii with a&nbsp;color.</figcaption></figure></div><h3>The Data</h3><p>Now, we get to the final piece of the puzzle: the data.</p><h4>Structure of&nbsp;data</h4><p>We need to structure the data in a way we can push it to the map while maintaining domain independence. All this means is that we shouldn&#8217;t have to change the code to make it work with another use case.</p><p>Suppose we are using this map to visualize the extent of coronavirus cases in the United States. Let&#8217;s make up some numbers and see what a subset of this data would look like:</p><pre><code>let positiveTests = {
  HI: 500,
  WA: 1000,
  MD: 10,
  FL: 9001
}</code></pre><p>Great! We clearly have a key-value pair of state code to value. Now, how do we bucket these into groups along a scale? I&#8217;m not a data algorithm expert, so we&#8217;ll do a bit of a janky approach.</p><h4>First calculate how we want to distribute it</h4><p>The first decision to make&#8202;&#8212;&#8202;how many buckets do we want?</p><p>Let&#8217;s suppose we want to have the following breakdown:</p><ul><li><p>High (&#8805; 5000) [Red]</p></li><li><p>Moderate (&#8805; 1000 and &lt; 5000) [Orange]</p></li><li><p>Low (&#8805; 500 and &lt; 1000) [Yellow]</p></li><li><p>Minimal (&lt; 500) [Green]</p></li><li><p>Unknown [Grey]</p></li></ul><p>We effectively have 5 buckets that we will divide the values into. This is what I will call the distribution scale (not the actual name since I&#8217;m not a data guy).</p><p>This distribution scale can be hard-coded as below:</p><pre><code>function getColorFor(value) {
   if (!value) {
     return 'grey';
   }
   else if (value &gt;= 5000) {
     return 'red';
   }
   else if (value &gt;= 1000) {
     return 'orange';
   }
   else if (value &gt;= 500) {
     return 'yellow'
   }
   else {
     return 'green';
   }
}</code></pre><p>Now, let&#8217;s incorporate this <code>getColorFor(value)</code> function into the <code>setColorFor(state)</code> function we created above in our <code>map.js</code> script:</p><pre><code>// map.js
let data = {
  HI: 500,
  WA: 1000,
  MD: 10,
  FL: 9001
}</code></pre><pre><code>function setColorFor(state) {
  let color = getColorFor(data[state]);
  let element = document.getElementById(state)
  if (element) {
    element.style.fill = color;
  }
}</code></pre><pre><code>function getColorFor(value) {
  // ...
}</code></pre><pre><code>setColorFor('HI');</code></pre><p>At this point, Hawaii should turn yellow&#8202;&#8212;&#8202;this makes sense because our data says that Hawaii has &#8805; 500 cases. However, all the other states in the dataset are still grey.</p><h4>Coloring the other&nbsp;states</h4><p>The other states are still grey because the coloring of states is hard-coded.</p><p>The <code>setColorFor('HI')</code> line is the line of code that is triggering the color change. We could go and write out every single state, but that is boring and just poor engineering.</p><p>Let&#8217;s go and make it so that we don&#8217;t have to list every state individually. Let&#8217;s remove <code>setColorFor('HI')</code> and replace it with the below code.</p><pre><code>//map.js
// ...all other code</code></pre><pre><code>function applyData(data) {
  Object.keys(data).forEach(function(key) {
    setColorFor(key);
  });
}</code></pre><pre><code>applyData(data);</code></pre><p>Now we should see all the states we included in our dataset light up like a rainbow:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lQdx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246810f1-1858-4bfb-bb7b-146a10bf12e7_800x514.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lQdx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246810f1-1858-4bfb-bb7b-146a10bf12e7_800x514.png 424w, https://substackcdn.com/image/fetch/$s_!lQdx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246810f1-1858-4bfb-bb7b-146a10bf12e7_800x514.png 848w, https://substackcdn.com/image/fetch/$s_!lQdx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246810f1-1858-4bfb-bb7b-146a10bf12e7_800x514.png 1272w, https://substackcdn.com/image/fetch/$s_!lQdx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246810f1-1858-4bfb-bb7b-146a10bf12e7_800x514.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lQdx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246810f1-1858-4bfb-bb7b-146a10bf12e7_800x514.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/246810f1-1858-4bfb-bb7b-146a10bf12e7_800x514.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lQdx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246810f1-1858-4bfb-bb7b-146a10bf12e7_800x514.png 424w, https://substackcdn.com/image/fetch/$s_!lQdx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246810f1-1858-4bfb-bb7b-146a10bf12e7_800x514.png 848w, https://substackcdn.com/image/fetch/$s_!lQdx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246810f1-1858-4bfb-bb7b-146a10bf12e7_800x514.png 1272w, https://substackcdn.com/image/fetch/$s_!lQdx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246810f1-1858-4bfb-bb7b-146a10bf12e7_800x514.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">applyData(data) will go and iterate through the object&#8217;s keys, which happen to be state abbreviations like &#8216;HI&#8217; or&nbsp;&#8216;WA&#8217;.</figcaption></figure></div><p>You can now see how you would start adding new colors, etc. It&#8217;s all very straightforward to do.</p><h3>Addressing other&nbsp;concerns</h3><p>There&#8217;s some other areas of concern you may encounter while attempting to do this in a production environment.</p><h4>Making it work with frameworks like Vue.js, React, or&nbsp;Angular</h4><p>I used vanilla JS and HTML in the examples to simply illustrate the concepts, but many modern web applications are written using actual frameworks and libraries.</p><p>An application of the approach in a production environment would likely involve many other aspects such as componentization, theming, etc. and leverage actual frameworks or libraries.</p><p>The great news is that you can easily follow the same exact technique and accomplish the goal regardless of the front-end technology. Vue, for example, allows you to use directive bindings directly on the SVG <code>&lt;path&gt;</code> elements.</p><p>Linked below is a Github repository I created illustrating the full approach describe above in vanilla Javascript, with an included sample of what it would look like as a Vue.js component:</p><p><strong><a href="https://github.com/JGefroh/example-heatmap">JGefroh/example-heatmap</a></strong><a href="https://github.com/JGefroh/example-heatmap"><br></a><em><a href="https://github.com/JGefroh/example-heatmap">This repository illustrates an example of how to create your own heatmap. Read my accompanying blog post for a&#8230;</a></em><a href="https://github.com/JGefroh/example-heatmap">github.com</a></p><h4>Automatically scaling the&nbsp;heatmap</h4><p>You may notice that, when resizing the browser window, your map doesn&#8217;t automatically adjust in size. You&#8217;ll need to do some extra configuration to make this work.</p><p>For the map above, adding a <code>viewbox</code> attribute with a value of <code>0 0 930 585</code> to the SVG element made it scale appropriately for me (designers may shriek in terror at the eyeballing I did). Different contexts and SVGs will require different scaling approaches.</p><p>If you want to learn all of the in-and-outs of SVG resizing and scaling, Amelia Bellamy-Royds wrote a fantastic article about in on CSS-Tricks:</p><p><strong><a href="https://css-tricks.com/scale-svg/">How to Scale SVG | CSS-Tricks</a></strong><a href="https://css-tricks.com/scale-svg/"><br></a><em><a href="https://css-tricks.com/scale-svg/">The following is a guest post by Amelia Bellamy-Royds. Amelia has lots of experience with SVG, as the co-author of SVG&#8230;</a></em><a href="https://css-tricks.com/scale-svg/">css-tricks.com</a></p><h4>Creating a linear distribution</h4><p>Perhaps you don&#8217;t want to pre-define the buckets, but rather divide the groups into a known number based on an interval that is dependent on the data.</p><p>You can do this by finding the maximum value in the dataset and then dividing it by the number of bucket you would like to have. This will give you the interval, which you could then color the sections by:</p><pre><code>function getInterval(numbers) {
  const NUMBER_OF_BUCKETS = 3;</code></pre><pre><code>  let maximum = Math.max.apply(Math, numbers);</code></pre><pre><code>  return maximum / NUMBER_OF_BUCKETS;
}</code></pre><pre><code>function getStyleFor(stateCode, valuesByStateCode) {
  let interval = getInterval(Object.values(valuesByStateCode));
  let value = valuesByStateCode[stateCode]</code></pre><pre><code>  if (value &lt;= interval) {
    return 'fill: #01C7E5';
  }
  else if (value &lt;= interval * 1) {
    return 'fill: #03C2DF'
  }
  else if { // ...and so on
  }</code></pre><pre><code>}</code></pre><h4>Dynamic color and data&nbsp;scales</h4><p>You can dynamically set the color or data scale by accepting an ordered array of colors, or a function that determines it.</p><p>If you truly wanted, you could make it fully data-driven. Something like below can be done:</p><pre><code>let valuesByStateCode = {
  HI: 30,
  WA: 50,
  FL: 2
};</code></pre><pre><code>let buckets = [
  { color: 'green' },
  { color: 'blue' },
  { color: 'red' },
];</code></pre><pre><code>function getInterval(numbers) {
  let maximum = Math.max.apply(Math, numbers);
  return maximum / buckets.length;
}</code></pre><pre><code>function getStyleFor(stateCode) {
  let interval = getInterval(Object.values(valuesByStateCode));
  let value = valuesByStateCode[stateCode]</code></pre><pre><code>  let bucketIndex = Math.floor(value / interval);
  let bucket = null;</code></pre><pre><code>  if (isNaN(value)) {
    bucket = buckets[0];
  }  
  else if (bucketIndex &gt;= buckets.length) {
    bucket = buckets[buckets.length - 1];
  }
  else {
    bucket = buckets[bucketIndex]
  }</code></pre><pre><code>  return `fill: ${bucket.color}`;
}</code></pre><p>This code accepts an ordered array of colors and automatically figures out the number of buckets to divide the values into. Once that is done, it buckets the values received into those colors automatically.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qp6F!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c703e8-cea6-4bf0-9dd5-c4cae44cc8b1_800x510.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qp6F!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c703e8-cea6-4bf0-9dd5-c4cae44cc8b1_800x510.png 424w, https://substackcdn.com/image/fetch/$s_!qp6F!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c703e8-cea6-4bf0-9dd5-c4cae44cc8b1_800x510.png 848w, https://substackcdn.com/image/fetch/$s_!qp6F!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c703e8-cea6-4bf0-9dd5-c4cae44cc8b1_800x510.png 1272w, https://substackcdn.com/image/fetch/$s_!qp6F!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c703e8-cea6-4bf0-9dd5-c4cae44cc8b1_800x510.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qp6F!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c703e8-cea6-4bf0-9dd5-c4cae44cc8b1_800x510.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/34c703e8-cea6-4bf0-9dd5-c4cae44cc8b1_800x510.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!qp6F!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c703e8-cea6-4bf0-9dd5-c4cae44cc8b1_800x510.png 424w, https://substackcdn.com/image/fetch/$s_!qp6F!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c703e8-cea6-4bf0-9dd5-c4cae44cc8b1_800x510.png 848w, https://substackcdn.com/image/fetch/$s_!qp6F!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c703e8-cea6-4bf0-9dd5-c4cae44cc8b1_800x510.png 1272w, https://substackcdn.com/image/fetch/$s_!qp6F!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c703e8-cea6-4bf0-9dd5-c4cae44cc8b1_800x510.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">A fully colored&nbsp;map.</figcaption></figure></div><p>You could then code further and create a gradient off a base color instead of having a pre-defined color scale if you wanted to, but I leave that as an exercise to the reader.</p><h3>And that&#8217;s about&nbsp;it!</h3><p>That&#8217;s the basics of creating a heatmap. Turns out you can do it in about half an hour and save yourself or your company thousands of dollars if cost is a concern and all you need are some basics.</p><p>Note that you aren&#8217;t limited to maps of the United States. The technique illustrated above will work for pretty much any heatmap-style coloring. As long as you have a properly constructed SVG, you&#8217;re good to go.</p><p>Did you like this article? Let me know in the comments, or connect with me on <a href="https://www.linkedin.com/in/jgefroh/">LinkedIn</a>!</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tORx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa079a461-fc8f-4d7b-8d86-95f2d11cb4b3_250x250.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tORx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa079a461-fc8f-4d7b-8d86-95f2d11cb4b3_250x250.png 424w, https://substackcdn.com/image/fetch/$s_!tORx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa079a461-fc8f-4d7b-8d86-95f2d11cb4b3_250x250.png 848w, https://substackcdn.com/image/fetch/$s_!tORx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa079a461-fc8f-4d7b-8d86-95f2d11cb4b3_250x250.png 1272w, https://substackcdn.com/image/fetch/$s_!tORx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa079a461-fc8f-4d7b-8d86-95f2d11cb4b3_250x250.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tORx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa079a461-fc8f-4d7b-8d86-95f2d11cb4b3_250x250.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a079a461-fc8f-4d7b-8d86-95f2d11cb4b3_250x250.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tORx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa079a461-fc8f-4d7b-8d86-95f2d11cb4b3_250x250.png 424w, https://substackcdn.com/image/fetch/$s_!tORx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa079a461-fc8f-4d7b-8d86-95f2d11cb4b3_250x250.png 848w, https://substackcdn.com/image/fetch/$s_!tORx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa079a461-fc8f-4d7b-8d86-95f2d11cb4b3_250x250.png 1272w, https://substackcdn.com/image/fetch/$s_!tORx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa079a461-fc8f-4d7b-8d86-95f2d11cb4b3_250x250.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div>]]></content:encoded></item><item><title><![CDATA[How to Design Software — Feature Flags]]></title><description><![CDATA[Take an in-depth look at how I designed, developed, and migrated a legacy system to a new feature flag system]]></description><link>https://blog.jgefroh.com/p/how-to-design-software-feature-flags-283c5f938171</link><guid isPermaLink="false">https://blog.jgefroh.com/p/how-to-design-software-feature-flags-283c5f938171</guid><dc:creator><![CDATA[Joseph Gefroh]]></dc:creator><pubDate>Tue, 14 Jul 2020 14:00:36 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d224fe7-ca42-4d4a-b5a3-527e9ade4214_250x250.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!BQLf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbff07ef1-d286-4ea5-aae5-1a1fee3ee243_800x418.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BQLf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbff07ef1-d286-4ea5-aae5-1a1fee3ee243_800x418.png 424w, https://substackcdn.com/image/fetch/$s_!BQLf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbff07ef1-d286-4ea5-aae5-1a1fee3ee243_800x418.png 848w, https://substackcdn.com/image/fetch/$s_!BQLf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbff07ef1-d286-4ea5-aae5-1a1fee3ee243_800x418.png 1272w, https://substackcdn.com/image/fetch/$s_!BQLf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbff07ef1-d286-4ea5-aae5-1a1fee3ee243_800x418.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!BQLf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbff07ef1-d286-4ea5-aae5-1a1fee3ee243_800x418.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bff07ef1-d286-4ea5-aae5-1a1fee3ee243_800x418.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!BQLf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbff07ef1-d286-4ea5-aae5-1a1fee3ee243_800x418.png 424w, https://substackcdn.com/image/fetch/$s_!BQLf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbff07ef1-d286-4ea5-aae5-1a1fee3ee243_800x418.png 848w, https://substackcdn.com/image/fetch/$s_!BQLf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbff07ef1-d286-4ea5-aae5-1a1fee3ee243_800x418.png 1272w, https://substackcdn.com/image/fetch/$s_!BQLf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbff07ef1-d286-4ea5-aae5-1a1fee3ee243_800x418.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><h4>Take an in-depth look at how I designed, developed, and migrated a legacy system to a new feature flag&nbsp;system</h4><p>A previous company had a problem: our deploys were thousands of lines in size, took nearly an hour, and were massively risky. Engineers hated deploying, which led to a backed-up queue of dozens of pull requests awaiting deployment. Product managers reasonably wanted to wait to release things until it was all done, which meant our releases could contain thousands of lines of code, any one of which could cause a catastrophic crash. The result? Incomplete work was piling up, value wasting away without delivery.</p><p>Something had to change.</p><h3>Releasing Deploys</h3><p>To help resolve these issues, one of the things I implemented was technology improvements that allowed us to separate the concept of a deploy from the concept of a release.</p><p>Previously, the deploy was the release, as soon as the code made it to production, it was being used by millions of users. This made deployments take on incredible risk.</p><p>This directly put engineers at odds with the product manager. Engineers wanted to minimize risk through smaller deploys, but product managers wanted to release working, comprehensive solutions. Instead of working together, our lack of technology capabilities created an environment with a combative dynamic.</p><p>We wanted to ensure that our technology could support releasing functionality at a later time than when it was deployed. Put another way, we wanted to be able to send code to production without users being able to see it, and then allow the business and product managers to own the release of features on their schedule.</p><p>To accomplish this separation, I pursued the development and integration of a centralized feature flagging system as a ninja project in-between my scheduled work.</p><h3>What Are Feature&nbsp;Flags?</h3><p>A feature flag system, also known as a feature toggler, boils down to storing whether a feature is enabled or not and then checking it when you need to. If the feature is enabled, allow whatever it is you are toggling. If not, hide it from users.</p><p>Code-wise, it theoretically resolves to boolean check and the execution (or not) of an accompanying code path. An example:</p><pre><code>run_code_path if flag</code></pre><h4>The flag</h4><p>The flag itself is a value that the code checks for in making its determination to run or not run a specific piece of code. This is the thing that the code would check for when making a runtime determination.</p><h4>The code&nbsp;path</h4><p>The code path is the code that is being toggled or disabled/enabled.</p><p>Perhaps it checks whether a page component can be displayed or not. Maybe it adds an extra fee to a transaction or not. Maybe it isn&#8217;t even completed, and you are just putting the skeleton in place.</p><p>These various reasons for hiding the code will determine the longevity of the flag, how it is used, and where it is placed, but the end mechanics are the same.</p><h4>The check</h4><p>The check is the code that determines whether the code should run or not. It&#8217;s the if-statement that will look at the flag and run the code path.</p><p>For us, we divided the system conceptually into two different kinds of checks: system-level checks and context-level checks.</p><ul><li><p><strong>System-level checks</strong> perform checks system-wide. If the check passed for one case, it would pass for all cases.</p></li><li><p><strong>Context-level checks</strong> perform checks within a specific context, such as within the scope of a User record. If the check passed for one context, it did not determine whether it would pass for another.</p></li></ul><p>Finally, checks could be layered. By combining flags for system-level and context-level, we could toggle functionality for features on specific records, or records belonging to a specific user, or records belonging to users within a specific group, the possibilities are endless.</p><h3>What Feature Flags Are&nbsp;Not</h3><p>One critical thing to point out is what feature flags specifically are not intended for.</p><p>Because it is a boolean check, you can be tempted to leverage it in any situation where you would have a boolean check. However, just because you can doesn&#8217;t mean you should. Developers can fall into the trap of using it for account-specific authorization related logic just because it exists. It is important to not mix this area of concern.</p><p>You should never use a feature flag system as an account permission check, such as checking if a user is an administrator or not. Even if the base concept of checking a flag is the same, everything else is different enough to warrant separate consideration. Security is important, and concepts like authorization should be addressed as a primary concern within your system, not relegated to an unrelated feature flag infrastructure.</p><h3>Dealing With Legacy&nbsp;Work</h3><p>The product already had a few false starts in this area scattered throughout the codebase.</p><p>The most common feature flag implementations I saw in this product, as well as many others throughout my career, fell into these three camps:</p><ul><li><p>A boolean on a record</p></li><li><p>A string array on a record</p></li><li><p>An environment variable check</p></li></ul><h4>The boolean&nbsp;column</h4><p>The boolean on a record is simple, add a boolean representing whether the feature is enabled, then check it:</p><pre><code>process_surcharge if @cause.surcharge_enabled?</code></pre><p>The downside of this approach is that if you have dozens of features, or flags that can be shared across multiple kinds of records, you&#8217;d end up with just as many feature flag columns all over your system, and various checks for them.</p><p>It would also clutter database tables with things specific to mechanics of how the system works, and not the domain. This can get unwieldy quickly.</p><h4>The string&nbsp;array</h4><p>The second approach was clearly designed to solve the first downside of supporting multiple features.</p><pre><code>process_surcharge if @cause.features.contains?('surcharge')</code></pre><p>It solves one problem but still has the other downsides. It also then introduced a problem of being inconsistent with the already established feature flag pattern, which hadn&#8217;t been ported over to the new approach.</p><p>Add in a couple of years of proliferation and we ended up with dozens of places where one or the other were being used.</p><h4><strong>The environment variable</strong></h4><p>Finally, system-wide features were flagged with environment variables.</p><pre><code>process_surcharge if ENV['IS_SURCHARGE_ENABLED']</code></pre><h4>Solving for legacy constraints</h4><p>Greenfield would allow us to do anything we wanted, but we live in a legacy world. Any new solution we built had to corral all of these various partial solutions to ensure there was only one true way of toggling a feature.</p><p>Otherwise, developer psychology would lead to developers continuing to create their own or copying one of the other feature toggling mechanisms due to the inconsistency.</p><h3>First Steps</h3><p>We started by addressing the legacy constraints. We solved the problem of all of these disparate methods of feature checking by adding a layer of indirection and consolidating all of the differences within that layer.</p><h4>The implementation</h4><p>We created a service class called <code>FeatureToggler</code> with a method called <code>enabled?</code> that accepted a flag and a record:</p><pre><code>class FeatureToggler
  def enabled?(flag, record)
  end
end</code></pre><p>Because different kinds of records in our system had a different way to check whether a feature was enabled or not, we enumerated all of those ways within the <code>enabled?</code> function:</p><pre><code>def enabled?(flag, record)
 return record.features.contains?(flag) if record.instance_of?(User)
 return record.send("#{flag}?") if record.instance_of?(Cause)
 # ...and so on...
end</code></pre><p>We then replaced all of the various <code>if x.&lt;y&gt;_enabled?</code>, <code>if z.features.contains?(a)</code> checks with calls to the new function.</p><p>The code movement screamed of violations of every kind from &#8220;don&#8217;t repeat yourself&#8221; to &#8220;single responsibility principle&#8221; to &#8220;breaking the rules of basic inheritance.&#8221;</p><p>It was all for the greater good, though. Sometimes you have to make the code a bit messier before you can clean it up. Like a sliding puzzle, you may need to do the &#8220;3 steps forward, 2 steps back&#8221; dance until you complete the refactor.</p><p>Minor updates to our existing test suite ensured it continued to pass. The new consolidated interface actually made it possible to split out the testing of feature toggling functionality from other tests and creating a specific suite just for that, which made testing flagging much easier and more comprehensive.</p><h3>A New Data&nbsp;Model</h3><p>Once the <code>FeatureToggler</code> abstraction layer was in place, we wanted to transition to a new data model, one that was divorced from all of the other data models within our system.</p><h4>The new data&nbsp;model</h4><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aSsp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F309e173f-88fe-45c2-8388-42b96a69af83_800x209.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aSsp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F309e173f-88fe-45c2-8388-42b96a69af83_800x209.png 424w, https://substackcdn.com/image/fetch/$s_!aSsp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F309e173f-88fe-45c2-8388-42b96a69af83_800x209.png 848w, https://substackcdn.com/image/fetch/$s_!aSsp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F309e173f-88fe-45c2-8388-42b96a69af83_800x209.png 1272w, https://substackcdn.com/image/fetch/$s_!aSsp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F309e173f-88fe-45c2-8388-42b96a69af83_800x209.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aSsp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F309e173f-88fe-45c2-8388-42b96a69af83_800x209.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/309e173f-88fe-45c2-8388-42b96a69af83_800x209.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!aSsp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F309e173f-88fe-45c2-8388-42b96a69af83_800x209.png 424w, https://substackcdn.com/image/fetch/$s_!aSsp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F309e173f-88fe-45c2-8388-42b96a69af83_800x209.png 848w, https://substackcdn.com/image/fetch/$s_!aSsp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F309e173f-88fe-45c2-8388-42b96a69af83_800x209.png 1272w, https://substackcdn.com/image/fetch/$s_!aSsp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F309e173f-88fe-45c2-8388-42b96a69af83_800x209.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The first data model for the feature&nbsp;flags.</figcaption></figure></div><p><strong>value<br></strong><code>value</code> was the flag itself, the value we would check in our code to determine if a flag was running or not.</p><p><strong>status<br></strong><code>status</code> was a string that represented whether the flag was <code>enabled</code> or <code>disabled</code>.</p><p><strong>owner_id and owner_type<br></strong><code>owner_id</code> and <code>owner_type</code> were two fields that tracked the owning record in a polymorphic manner. If the owner was a Contract record with ID 2, <code>owner_id</code> would be <code>2</code> and <code>owner_type</code> would be <code>Contract</code>.</p><p>Having no owner meant that the flag was intended to be a system flag, not a context flag.</p><h3>Transitioning to the New Data&nbsp;Model</h3><p>Transitioning was a bit more complex than we desired. Our transition plan required us to transition two aspects of flags: reads and writes.</p><h4>Reads were&nbsp;easy</h4><p>Reads were straightforward, we could insert a check in our new <code>FeatureToggler</code> layer to look at the new data model:</p><pre><code>def enabled?(flag, record)
 FeatureFlag.exists?(owner: record, enabled: :true, value: flag)
end</code></pre><h4>Writes were a bit more challenging</h4><p>Writes were made harder due to legacy constraints.</p><p>In order to allow operations to toggle features on our old system, we had a CRUD interface that was data-model aware and generated via DSL. This meant that it was hard-coded and specifically knew whether it was inserting into an array, setting a boolean on a column on a specific record.</p><p>There was no way to abstract that behavior, we were severely limited in our ability to change it out. If we added a flag using the new data model, it would not reflect in the old one. If we added a flag in the old data model, it would not reflect in the new one. This meant flags could go out of sync. It was a potential source of significant confusion.</p><p>Legacy constraints were forcing us to head down the path of an all-or-nothing release, which was explicitly an anti-goal of our initiative.</p><p>We couldn&#8217;t have both systems being written-to in parallel, which meant we had to do a complete transition if we wanted to migrate writes.</p><h4>The solution: Only transition reads</h4><p>We decided to not migrate writes just yet and only migrate the reads. We kept the old data model as the source of truth for changes by propagating any changes made to them down to the new data model.</p><p>We wrote a migration to take all of the feature flags we had stored in the other various approaches and copied them to the new data model.</p><p>To keep the data between the old and the new data model in sync, we added an extra step to the existing writes through callbacks, ensuring we were also writing the same data to the new data model in the various locations flags were being written using the approaches.</p><p>Finally, we changed the reads of feature flags to point to the new system, but to also verify results via a quorum system that logged when the checks disagreed:</p><pre><code>def enabled?(flag, record)
 if new_enabled?(flag, record) == old_enabled?(flag, record)
   return new_enabled?(flag, record)
 else
   log_quorum_failure(flag, record)
   return old_enabled?(flag, record)
  end
end</code></pre><p>We also decided to eat our own dog food and feature flag the feature flag check to determine whether to use the old system or the new system as a just-in-case. Talk about extra safe.</p><p>By removing the requirement of transitioning writes, we greatly simplified the project and it allowed us to deliver a small piece of value rapidly. Sometimes the simplest thing is to do nothing.</p><h4>Flawless victory</h4><p>We ran the system in production for a while, and it ran perfectly, we didn&#8217;t see any disagreements being logged, except for those we intentionally created as a test.</p><p>At that point, we switched over to using the new data model for all of our reads and started looking at transitioning writes.</p><h3>Transitioning Writes</h3><p>Because we had full confidence in the stability of the reads and the usefulness of the new system as a whole, we approached migrating writes with a whole lot more confidence.</p><p>We had two data models in play, with the old data model being the source of truth for changes. Since changes to the old data model got synced to the new data model, this meant we were still reliant on all of the limitations of the old approach.</p><p>We needed to make our new data model the source of changes. Because our old system was reliant on an inflexible DSL for feature flag modification, this meant that we had to create a new user interface to provide the same functionality that was expected before we could actually transition writes over.</p><p>Turns out it was a piece of cake with the new data model. Operationally supporting the new flagging data model was as complex as inserting and reading records in a table. There were no special rules or gotchas. The interface literally boiled down to a table with some buttons, it was CRUD functionality in its purest form.</p><h4>Releasing</h4><p>The actual release was pretty straightforward.</p><p>We provided a brief training to the main group of people who modified feature flags, wrote and shared a one-sheet to document it extensively, and then provided links to the new interface where the old interfaces were.</p><h3>Iterations and Improvements</h3><p>Our new system was fantastic.</p><p>It allowed us to rapidly apply different kinds of feature flags or apply multiple flags within different contexts without the need for database migrations.</p><p>Cascading flag checks were now easy, we could toggle functionality based on a specific record or for a specific user. We didn&#8217;t stop there. Now that we weren&#8217;t limited by legacy constraints, we could quickly add more functionality as time went on.</p><h4>Descriptive enumerations</h4><p>One problem with the old feature flag approach was that nobody knew what the flags actually did or meant.</p><p>Because many of them were boolean fields or string values without descriptions, it was difficult to tell exactly what the consequences of toggling them were. Unless you were already familiar with the system, you would never be able to find out.</p><p>When we transitioned them over, we preserved their unfortunate naming.</p><p>For non-engineers, flags with values like <code>bpfee</code> and <code>gp</code> were indecipherable. It provided a terrible user experience and led to a lot of operational errors that ultimately made their way back to engineering in the form of bug reports and investigations, which we wanted to prevent.</p><p>We wanted to clearly describe what flags did and what they meant, so we introduced a data model to list what the available options were:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KXtT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f8355e-3b92-4f8a-9165-b19bb7eb4034_800x208.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KXtT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f8355e-3b92-4f8a-9165-b19bb7eb4034_800x208.png 424w, https://substackcdn.com/image/fetch/$s_!KXtT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f8355e-3b92-4f8a-9165-b19bb7eb4034_800x208.png 848w, https://substackcdn.com/image/fetch/$s_!KXtT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f8355e-3b92-4f8a-9165-b19bb7eb4034_800x208.png 1272w, https://substackcdn.com/image/fetch/$s_!KXtT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f8355e-3b92-4f8a-9165-b19bb7eb4034_800x208.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KXtT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f8355e-3b92-4f8a-9165-b19bb7eb4034_800x208.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d0f8355e-3b92-4f8a-9165-b19bb7eb4034_800x208.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!KXtT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f8355e-3b92-4f8a-9165-b19bb7eb4034_800x208.png 424w, https://substackcdn.com/image/fetch/$s_!KXtT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f8355e-3b92-4f8a-9165-b19bb7eb4034_800x208.png 848w, https://substackcdn.com/image/fetch/$s_!KXtT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f8355e-3b92-4f8a-9165-b19bb7eb4034_800x208.png 1272w, https://substackcdn.com/image/fetch/$s_!KXtT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f8355e-3b92-4f8a-9165-b19bb7eb4034_800x208.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Feature Flag&nbsp;Options</figcaption></figure></div><p>We changed our modification interface to decorate the list of feature flags with these details, providing significantly more clarity to anyone modifying the feature flags.</p><p>They no longer had to see just:</p><pre><code>bpfee</code></pre><p>They also saw what it meant:</p><pre><code>Receipt Basis Points Fee (bpfee)</code></pre><pre><code>Display fee percentages charged as basis points on the receipt.</code></pre><p>The increased clarity led to significantly fewer errors and questions reaching us, resolving a large source of upstream issues.</p><h4>Defaults</h4><p>Sometimes when a record gets created we want certain flags to be automatically enabled. By adding a column to the <code>FeatureFlagOption</code> table, <code>default_status</code>, we can achieve this:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!CCA2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf30291f-40b6-4d71-bad7-36d7147e10b7_800x254.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CCA2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf30291f-40b6-4d71-bad7-36d7147e10b7_800x254.png 424w, https://substackcdn.com/image/fetch/$s_!CCA2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf30291f-40b6-4d71-bad7-36d7147e10b7_800x254.png 848w, https://substackcdn.com/image/fetch/$s_!CCA2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf30291f-40b6-4d71-bad7-36d7147e10b7_800x254.png 1272w, https://substackcdn.com/image/fetch/$s_!CCA2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf30291f-40b6-4d71-bad7-36d7147e10b7_800x254.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!CCA2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf30291f-40b6-4d71-bad7-36d7147e10b7_800x254.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cf30291f-40b6-4d71-bad7-36d7147e10b7_800x254.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!CCA2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf30291f-40b6-4d71-bad7-36d7147e10b7_800x254.png 424w, https://substackcdn.com/image/fetch/$s_!CCA2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf30291f-40b6-4d71-bad7-36d7147e10b7_800x254.png 848w, https://substackcdn.com/image/fetch/$s_!CCA2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf30291f-40b6-4d71-bad7-36d7147e10b7_800x254.png 1272w, https://substackcdn.com/image/fetch/$s_!CCA2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf30291f-40b6-4d71-bad7-36d7147e10b7_800x254.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Feature Flag&nbsp;Options</figcaption></figure></div><p>Now, whenever a record was created, we could pull up all of the flags where <code>default_status </code>was <code>enabled</code> and automatically enable the flags on that record.</p><h4>Categorization</h4><p>Over time, you get a lot of different feature flags, all for different purposes.</p><p>Some are flags intended for short-term use, typically to hide code until it is done or to mitigate risk by performing canary releases. Once the code is deployed, the flag should get removed. Some are flags intended for mid-term use, such as those used to perform experiments. Some flags are intended for long-term use. Features might be toggled based on the contract or plan the customer is on.</p><p>Each of these flags have different owners and usages, so it is important to be able to separate them. By adding a <code>category</code> to <code>FeatureFlagOption</code>, we are able to scope the enumeration and behavior even further:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cKCD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fd49ae3-a593-49c4-b94a-63c1a40a45a6_800x275.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cKCD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fd49ae3-a593-49c4-b94a-63c1a40a45a6_800x275.png 424w, https://substackcdn.com/image/fetch/$s_!cKCD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fd49ae3-a593-49c4-b94a-63c1a40a45a6_800x275.png 848w, https://substackcdn.com/image/fetch/$s_!cKCD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fd49ae3-a593-49c4-b94a-63c1a40a45a6_800x275.png 1272w, https://substackcdn.com/image/fetch/$s_!cKCD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fd49ae3-a593-49c4-b94a-63c1a40a45a6_800x275.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cKCD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fd49ae3-a593-49c4-b94a-63c1a40a45a6_800x275.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0fd49ae3-a593-49c4-b94a-63c1a40a45a6_800x275.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!cKCD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fd49ae3-a593-49c4-b94a-63c1a40a45a6_800x275.png 424w, https://substackcdn.com/image/fetch/$s_!cKCD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fd49ae3-a593-49c4-b94a-63c1a40a45a6_800x275.png 848w, https://substackcdn.com/image/fetch/$s_!cKCD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fd49ae3-a593-49c4-b94a-63c1a40a45a6_800x275.png 1272w, https://substackcdn.com/image/fetch/$s_!cKCD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fd49ae3-a593-49c4-b94a-63c1a40a45a6_800x275.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Adding a category allowed us to scope the options appropriately depending on audience and&nbsp;usage.</figcaption></figure></div><h4>Experimentation</h4><p>Sometimes, we want flags that split groups into different cohorts for A/B testing of features. We could store the various parameters of experiments in the <code>FeatureFlagOption</code>, to be sent to the A/B subsystem:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!iRDS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32c778f3-2daf-4179-96ae-8600a8f57cfc_800x225.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!iRDS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32c778f3-2daf-4179-96ae-8600a8f57cfc_800x225.png 424w, https://substackcdn.com/image/fetch/$s_!iRDS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32c778f3-2daf-4179-96ae-8600a8f57cfc_800x225.png 848w, https://substackcdn.com/image/fetch/$s_!iRDS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32c778f3-2daf-4179-96ae-8600a8f57cfc_800x225.png 1272w, https://substackcdn.com/image/fetch/$s_!iRDS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32c778f3-2daf-4179-96ae-8600a8f57cfc_800x225.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!iRDS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32c778f3-2daf-4179-96ae-8600a8f57cfc_800x225.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/32c778f3-2daf-4179-96ae-8600a8f57cfc_800x225.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!iRDS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32c778f3-2daf-4179-96ae-8600a8f57cfc_800x225.png 424w, https://substackcdn.com/image/fetch/$s_!iRDS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32c778f3-2daf-4179-96ae-8600a8f57cfc_800x225.png 848w, https://substackcdn.com/image/fetch/$s_!iRDS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32c778f3-2daf-4179-96ae-8600a8f57cfc_800x225.png 1272w, https://substackcdn.com/image/fetch/$s_!iRDS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32c778f3-2daf-4179-96ae-8600a8f57cfc_800x225.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">By allowing for the storage of optional parameters in a flag, you can customize behavior of a&nbsp;flag.</figcaption></figure></div><p>While an A/B system is outside the scope of this post, I recommend checking out the<a href="https://github.com/splitrb/split"> split gem</a> if you are using ruby.</p><h4>Performance</h4><p>As usage of the feature flag system grows, you don&#8217;t want to be performing that many database reads. The abstraction layer provides a perfect place to put in a caching layer as well to ensure rapid lookups of feature flags.</p><p>As always, remember the words of the famed Donald Knuth:</p><blockquote><p>We <em>should</em> forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.</p></blockquote><p>Ensure you have an actual need to improve performance and that the usage patterns are appropriate for your solution.</p><p>As another tip: don&#8217;t randomly add caching logic ad-hoc. It quickly gets out of hand when you try and figure out whether you are dealing with cached data or not. It is made monumentally more complex when caching layers on the controller, data, and database all start interacting with each other.</p><p>Add caching thoughtfully and with centralized controls so you know exactly how stale the data is and how to force a refresh, if necessary.</p><h4>Client-side checks</h4><p>Our new technology platform greatly leveraged a lot of the feature flag infrastructure. We created a front-end service and component that showed/hid nested components based on the feature flag&#8217;s status.</p><p>By exposing flag checks in an API and then consolidating all of its logic within the service and component, we were able to avoid having feature flag logic all over the front-end.</p><h3>The Effects</h3><p>The transition went incredibly smoothly, and it was shocking how much separating releases from deploys improved team collaboration and delivery effectiveness.</p><p>Deploy sizes went down and frequency went up because developers could deploy code without causing production issues. This in turn decreased the change failure rate since deploys were smaller and bugs were easier to catch before they made it to production.</p><p>Releases went more smoothly and adapted better to the business&#8217; timetable because it didn&#8217;t require engineers to keep track of different branches of work and deal with integration challenges for tens of thousands of lines of code.</p><p>It wasn&#8217;t a silver bullet to all of our problems, but it helped streamline processes and remove a lot of collaboration friction, paying off dividends.</p><h3>Frequently Asked Questions</h3><h4>Why didn&#8217;t we go with a&nbsp;vendor?</h4><p>There&#8217;s a lot of vendors that provide flag functionality like Optimizely or LaunchDarkly. Why didn&#8217;t we go with them, why reinvent the wheel? In a word: budget.</p><p>At the time, we were under significant budget restrictions, and it was difficult to get any additional spending approved. As engineering didn&#8217;t control our budget, we had to make do with what we had.</p><p>Even getting resources to work on technical debt like this was impossible, we had to ensure we made progress on engineering initiatives during the time we found in the &#8220;in-between:&#8221; the small slices of time between tickets, projects, and meetings.</p><p>We had to work within the context we had.</p><h4>Why didn&#8217;t we go with a library or&nbsp;gem?</h4><p>We examined a lot of gems and realized that none of them suited our particular desires for future use cases, legacy constraints, or migration requirements. Several gems in particular would have required a big-bang approach to deployment, which was a level of risk we didn&#8217;t want to accept.</p><p>A few would have required a complete overhaul of how teams external to our department understood flagging, which expanded the scope of the changes we wanted to make and increased the delivery burden significantly by adding more stakeholders. We were already working on this as an unofficial ninja-project. We didn&#8217;t want to increase the odds of it never being delivered.</p><p>Most of the gems we looked at would have required us to go the route we did anyways. We decided to kick the selection down the road and roll our own quickly, but hide all-access behind an interface so we could swap out the implementation in the future if resources freed up.</p><p>We were already working on this as an unofficial ninja-project. We didn&#8217;t want to increase the odds of it never being delivered.</p><h4>Why didn&#8217;t you just transition everything over at&nbsp;once?</h4><p>An all-or-nothing approach was not desired for a few reasons.</p><p>Recall that this was a ninja project we were doing in-between actual work. This meant I had a few minutes here, an hour or two there to actually complete this migration. An all-or-nothing approach would have required significantly more focus than we could afford to allot.</p><p>We did introduce extra complexity in the migration by doing it in smaller pieces. However, we de-risked almost the entire initiative by doing so. Feature flags were used extensively within the system for a variety of reasons including contractual obligations, and an error in it would have been highly consequential.</p><p>Doing it in smaller pieces was absolutely worth the safety, even if it ultimately wasn&#8217;t needed in the end.</p><h3>Where Do You Go From&nbsp;Here?</h3><p>If you&#8217;re interested in learning more about feature toggling, Martin Fowler&#8217;s site has an in-depth article written by Pete Hodgson on feature toggling which I highly recommend.</p><p><strong><a href="https://martinfowler.com/articles/feature-toggles.html">Feature Toggles (aka Feature Flags)</a></strong><a href="https://martinfowler.com/articles/feature-toggles.html"><br></a><em><a href="https://martinfowler.com/articles/feature-toggles.html">Pete Hodgson Pete Hodgson is an independent software delivery consultant based in the San Francisco Bay Area. He&#8230;</a></em><a href="https://martinfowler.com/articles/feature-toggles.html">martinfowler.com</a></p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hZ6D!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d224fe7-ca42-4d4a-b5a3-527e9ade4214_250x250.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hZ6D!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d224fe7-ca42-4d4a-b5a3-527e9ade4214_250x250.png 424w, https://substackcdn.com/image/fetch/$s_!hZ6D!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d224fe7-ca42-4d4a-b5a3-527e9ade4214_250x250.png 848w, https://substackcdn.com/image/fetch/$s_!hZ6D!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d224fe7-ca42-4d4a-b5a3-527e9ade4214_250x250.png 1272w, https://substackcdn.com/image/fetch/$s_!hZ6D!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d224fe7-ca42-4d4a-b5a3-527e9ade4214_250x250.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!hZ6D!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d224fe7-ca42-4d4a-b5a3-527e9ade4214_250x250.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5d224fe7-ca42-4d4a-b5a3-527e9ade4214_250x250.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!hZ6D!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d224fe7-ca42-4d4a-b5a3-527e9ade4214_250x250.png 424w, https://substackcdn.com/image/fetch/$s_!hZ6D!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d224fe7-ca42-4d4a-b5a3-527e9ade4214_250x250.png 848w, https://substackcdn.com/image/fetch/$s_!hZ6D!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d224fe7-ca42-4d4a-b5a3-527e9ade4214_250x250.png 1272w, https://substackcdn.com/image/fetch/$s_!hZ6D!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d224fe7-ca42-4d4a-b5a3-527e9ade4214_250x250.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div>]]></content:encoded></item><item><title><![CDATA[How to Design Software — Rules Engines]]></title><description><![CDATA[Walk through the components of a minimal rules engine and understand some of the thought processes behind approaching its design.]]></description><link>https://blog.jgefroh.com/p/how-to-design-software-rules-engines-adbb098b2d73</link><guid isPermaLink="false">https://blog.jgefroh.com/p/how-to-design-software-rules-engines-adbb098b2d73</guid><dc:creator><![CDATA[Joseph Gefroh]]></dc:creator><pubDate>Thu, 09 Jul 2020 16:33:41 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe514a521-16ce-42fc-bbac-0ef09f2b3dbc_250x250.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!siKg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7c9533c-0fd7-405f-8ab1-732ce3fe406e_800x418.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!siKg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7c9533c-0fd7-405f-8ab1-732ce3fe406e_800x418.png 424w, https://substackcdn.com/image/fetch/$s_!siKg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7c9533c-0fd7-405f-8ab1-732ce3fe406e_800x418.png 848w, https://substackcdn.com/image/fetch/$s_!siKg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7c9533c-0fd7-405f-8ab1-732ce3fe406e_800x418.png 1272w, https://substackcdn.com/image/fetch/$s_!siKg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7c9533c-0fd7-405f-8ab1-732ce3fe406e_800x418.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!siKg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7c9533c-0fd7-405f-8ab1-732ce3fe406e_800x418.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c7c9533c-0fd7-405f-8ab1-732ce3fe406e_800x418.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!siKg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7c9533c-0fd7-405f-8ab1-732ce3fe406e_800x418.png 424w, https://substackcdn.com/image/fetch/$s_!siKg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7c9533c-0fd7-405f-8ab1-732ce3fe406e_800x418.png 848w, https://substackcdn.com/image/fetch/$s_!siKg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7c9533c-0fd7-405f-8ab1-732ce3fe406e_800x418.png 1272w, https://substackcdn.com/image/fetch/$s_!siKg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7c9533c-0fd7-405f-8ab1-732ce3fe406e_800x418.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><h4>Walk through the components of a minimal rules engine and understand some of the thought processes behind approaching its&nbsp;design.</h4><h3>Imagine this&nbsp;scenario</h3><p>You&#8217;re a busy engineer working in a company with too much to build and too few resources to do it. Departments constantly ask for changes only an engineer can make. Marketing continuously asks for email on-boarding changes. Operations wants more and more batch tooling. Sales wants to test new pricing structures every other week. You and your team are overwhelmed with requests.</p><p>What are your options?</p><p><strong>Stop progress on planned work to make these requested changes?</strong> A constant stream of interruptions is terrible way to get any real work done, and who knows if these asks are even worth interrupting the <em>real</em> work for.</p><p><strong>Ignore it? </strong>You&#8217;ll make progress on your planned work, but you&#8217;ll also have a bunch of external stakeholders now suddenly very angry at your department. Besides, isn&#8217;t technology supposed to help the business? Is it really helpful to have the business miss out on potentially significant opportunities just because you couldn&#8217;t spare an hour or two?</p><p><strong>Triage the asks around a planned schedule?</strong> Planned interruptions are better than unplanned ones, but you&#8217;re still potentially limiting the progress of other departments and forcing them to work around your schedule&#8202;&#8212;&#8202;not quite as collaborative as one could be.</p><p>Imagine if there was a way to prevent these requirements from every reaching you, and to give the other departments tools to pursue these opportunities without your involvement. They&#8217;d be able to make internal pivots without having to rely on engineering to perform the work.</p><p>If such a solution existed, you&#8217;d save a lot of time and be able to focus on more important problems.</p><p>Enter the rules engine.</p><h3>What is a rules&nbsp;engine?</h3><p>A rules engine is a system that performs a set of actions based on specific conditions which can be configured during runtime. This means that an effectively set up rules engine would not require engineers to change a system&#8217;s business logic.</p><p>Engineers build rules-based systems all the time, albeit unconsciously. Every time you code an if-else statement, you are effectively creating a hardcoded rule that is followed by the system.</p><p>You can go a step further and make these systems configurable <em>dynamically</em>. When I say dynamic, I mean that the behavior of the system is not determined by flows coded by engineers, but by configuration through data.</p><p>These so-called &#8220;codeless&#8221; systems are not new. Software like Zapier, Hubspot, and IFTTT operate off of the same concepts, as do more technical implementations like Boomi or Drools.</p><blockquote><p>A rules engine is a system that performs a set of actions based on specific conditions which can be configured during&nbsp;runtime.</p></blockquote><h3><strong>The fragility of&nbsp;concrete</strong></h3><p>Why are rules engines even needed?</p><p>By default, most engineers concern themselves on the details of what the rules are of the code they are writing. They were concrete implementations specific to the business use case they are encountering.</p><p>This makes sense&#8202;&#8212;&#8202;code does exactly what it says it does, so engineers need to know what needs to be done to write the code properly.</p><p>However, this coupling to the details of the use case makes the code they write churn significantly when the details of the use case changes. While this approach works in many cases, sometimes the rate of change can become overwhelming, especially on smaller teams or more dynamic environments like post-investment startups.</p><p>It&#8217;s easy to see how these things can change. Business logic is implemented to fulfill a requirement in what behavior the software should exhibit. These requirements can come from many sources&#8202;&#8212;&#8202;business demands, improvements in UX, regulatory needs, technical drivers, etc.</p><p>Examples include:</p><ul><li><p>A &#8220;welcome&#8221; email must be sent 2 hours after a user signs up</p></li><li><p>If a payment account holds more than $100,000, over 3 consecutive days, the entire amount must be disbursed immediately</p></li><li><p>Employees that are a member of this union should accrue vacation time at double the standard rate for 2019, but not 2021.</p></li></ul><p>All of these can also vary in levels of stability, or how often the details change.</p><p>For example, a requirement based on data obtained through user analytics may change weekly, whereas a requirement based on a financial regulation may change once every couple of years. A requirement based on a union contract may only change when the contract is renegotiated.</p><p>When it does change, the implementation must change alongside of it.</p><h3>The evolution of an implementation</h3><p>Let&#8217;s examine a hypothetical implementation of the &#8220;welcome&#8221; email requirement as it evolves over time.</p><blockquote><p>A &#8220;welcome&#8221; email must be sent 2 hours after a user signs up</p></blockquote><h4>The first&nbsp;stage</h4><p>Developers can be relied on to do the easiest thing. Most engineers boil business logic down to an if-this-do-that: if this happens, perform this action.</p><p>The first (and unfortunately often last) implementation most developers take is the direct approach.</p><p>It&#8217;s so easy&#8202;&#8212;&#8202;a single line of code can fulfill this requirement:</p><pre><code>def on_user_create
   WelcomeEmail.send(in: 2.hours)
end</code></pre><h4>The second&nbsp;stage</h4><p>As time passes, the business often demands changes to details that seemed concrete and set in stone in the past.</p><p>Change is a constant, and software is SOFTware for a reason&#8202;&#8212;&#8202;it has to be malleable to fit the situation and circumstances.</p><p>What if we discover that 2 hours is too long, and we want to change it to 15 minutes? If we do, an engineer needs to go and change it.</p><pre><code>def after_user_create
   WelcomeEmail.send(in: 15.minutes)
end</code></pre><blockquote><p>Change is a constant, and software is SOFTware for a reason&#8202;&#8212;&#8202;it has to be malleable to fit the situation and circumstances.</p></blockquote><h4>The third&nbsp;stage</h4><p>If the business is performing experiments to see what kind of on-boarding leads to the best retention and conversion rates, it&#8217;s likely this value will change a lot.</p><p>If this happens enough times, the developer will hopefully get smart and move this detail out of the code so it can be configured during runtime.</p><pre><code>def after_user_create
   WelcomeEmail.send(in: Configuration.get('welcome_email_wait'))
end</code></pre><p><code>Configuration.get</code> could access the database, read a configuration file, look at an environment variable, or perhaps even randomly decide. The important factor is that it is now determined at runtime instead of hard-coded.</p><h4>The fourth&nbsp;stage</h4><p>Each individual requirement taken in isolation often ends at the third stage.</p><div class="paywall-jump" data-component-name="PaywallToDOM"></div><p>However, if there is a collection of related requirements, patterns can start to emerge which the intuitive engineer can pick up on.</p><p>Let&#8217;s say the business also wants to send a tips and tricks email. An engineer is likely to code the following implementation, being consistent with prior work:</p><pre><code>def after_user_create
   WelcomeEmail.send(in: Configuration.get('welcome_email_wait'))
   TipsEmail.send(in: Configuration.get('tips_email_wait'))
end</code></pre><p>Once again, however, it requires a engineer to add support for the new kind of email.</p><h4>Examining the evolution</h4><p>As we&#8217;ve seen above&#8202;&#8212;&#8202;each change is easy and small, but it adds up over time, quite insidiously. It&#8217;s easy to see how it can quickly grow out of control in a system where hundreds or thousands of requirements may be implemented in parallel, often under high delivery pressure.</p><p>Even in this simple circumstance, time has caused unrelated emails to be tightly coupled to record lifecycle, which makes systems more complicated.</p><p>In the above scenario, the developer made the judgement to couple the mechanism to the use case. This means the developer tied together <em>what </em>was being done to <em>how</em> something should be done to <em>why </em>something was being done. They coupled the business domain to the technical domain&#8202;&#8212;&#8202;the mechanism became tied to the use case.</p><blockquote><p>They coupled the business domain to the technical domain&#8202;&#8212;&#8202;the mechanism became tied to the use&nbsp;case.</p></blockquote><p>When we introduced the tips and tricks email, we essentially had to repeat the implementation. The implementation relied on specific details&#8202;&#8212;&#8202;namely when something was being done, how long to wait, and what was actually being sent. These details then changed, forcing changes to the implementation (aka. extra work for engineers).</p><h4>The details don&#8217;t&nbsp;matter</h4><p>The truth is that these details don&#8217;t matter.</p><p>From a technical perspective, it shouldn&#8217;t matter whether a welcome email is sent 2 hours after the user signs up or 15 minutes after. Nor should it matter that it was a welcome email or a tips and trick email.</p><p>That&#8217;s the realm of the business&#8202;&#8212;&#8202;these are merely use cases for the base technology.</p><p>The implementation itself should remain the same&#8202;&#8212;&#8202;as far as the system should be concerned, it sent a Foobar notification upon creation of a Whatsittoya record, and can use (mostly) the same exact code to send a Fizzbuzz notifcation upon the deletion of a Whatchamacallit record.</p><p>When it is sent or what is being sent are details that just aren&#8217;t as concrete. These are the details that should be abstracted away because these are the details that are the most likely to change over time.</p><p>You don&#8217;t want to have to engineer something every time a requirement changes. You want to give that power to the business, barring some contractual, security[1], or regulatory requirement.</p><p>[1] Security also includes job security.</p><h3>Building a rules&nbsp;engine</h3><p>So, now that we&#8217;ve examined the context in which a rules engine would be useful, how do you actually build one?</p><p>There&#8217;s a lot of ways, but conceptually a minimal rules engine includes the following components:</p><h4>Rule</h4><p>A rule is a business policy. Within the technical domain of the rules engine, it is a collection of a set of triggers, conditions, and effects that are applied by the rules engine.</p><h4>Trigger</h4><p>The trigger is the thing that determines whether the engine should attempt to run through a rule or not. In most cases, it is contextual.</p><p>In simple systems, it could be a simple string check or even hard-coded, such as a string on a Rule with the value <code>on_create</code> and <code>PaymentAccount</code> to indicate that the rule should only be executed when a record of type <code>PaymentAccount</code> is created.</p><p>In more complex systems, it could be a full context check that looks at things like whether the user is logged in or the kind of record being worked on.</p><h4>Condition</h4><p>The Condition determines if the Rule should be applied in that particular circumstance and on that particular record.</p><p>It has some minor overlap conceptually with a trigger, but also enough differences to warrant a separate discussion.</p><p>A trigger is more of a generalized determination of whether a rule&#8217;s conditions should even be checked, whereas condition is more of an instance-specific check. Smaller systems can potentially fold the trigger in to a a condition, but more complex systems will likely benefit from separating the two.</p><p>The Condition itself will likely have some sort of reference to actual code that performs the check itself, with some parameters to pass in to the function.</p><p>For example, you could store a Condition record in the database that stores the name of the condition class as well as some accompanying parameters. The code could then dynamically initialize the condition class it identifies, passing in the parameters and the record to check against.</p><h4>Effect</h4><p>The Effect is what happens once a Rule is triggered and its Conditions pass. It is typically a function or execution of a function. It may be something as simple as setting a field or something as complex as kicking off an entire workflow.</p><p>Like the Condition, it will likely have some sort of reference to actual code that applies the Effect itself.</p><h4>Engine</h4><p>Finally, you have the engine itself. This is the thing that will actually perform the bulk of the work. It&#8217;ll accept records, load a list of rules, check whether those rules should be applied based on specific triggers and conditions, and then apply the effects of the rules.</p><h4>Some other areas of&nbsp;concern</h4><p>In addition to the above components, you&#8217;ll also likely encounter secondary areas of concern when building the rules engine. Things like auditing changes to rules, tracking the history of rules being applied, enabling the configuration of rules to specific people, event-based effects, rule DSLs, etc. are all related subsystems, but also outside the scope of the core of the rules engine.</p><h3>The flow of a rules&nbsp;engine</h3><p>How would this rules engine actually work?</p><ul><li><p>Step 1: Trigger the engine</p></li><li><p>Step 2: Get the Rules</p></li><li><p>Step 3: Check the Conditions</p></li><li><p>Step 4: Apply the Effects</p></li></ul><h4>Step 1: Trigger the&nbsp;Engine</h4><p>The first step that occurs is the engine gets triggered. The entry point could be a function <code>Engine#run</code>. This function is passed a record and its associated context (eg. if the record is newly created, or the engine is being called in an update, etc.)</p><h4>Step 2: Get the&nbsp;Rules</h4><p>The engine will get the list of rules that apply for that specific context. Perhaps it will load it in real-time from the database. Maybe it pre-loaded it from a configuration file when the system booted.</p><p>Either way, these Rules will have associations to Conditions and Effects, which is what the important part is for the next couple of steps.</p><h4>Step 3: Check the Conditions</h4><p>The Condition would be a boolean check that determines whether the Rule&#8217;s Effects should be applied or not. If a Rule has multiple Conditions, further boolean logic should be performed to determine how the Conditions are evaluated to determine whether the Rule applies or not (eg. all or nothing, any, quorum).</p><h4>Step 4: Apply the&nbsp;Effects</h4><p>If the Rule&#8217;s Conditions pass, it is time to apply the Effects. The Effect itself is arbitrary. You&#8216;ll want to determine how to handle cases in which the application of an Effect failed.</p><p>Once this happens, the Rule has been applied. You&#8217;ve done it!</p><h3>A concrete&nbsp;example</h3><p>Suppose you had to set up a user account to be marked as a &#8220;Popular&#8221; member once a certain number of views for the profile has been reached. You could theoretically hard-code this, but it would require an engineer to change in the future. Good engineering minimizes the cost and impact of likely future changes.</p><p>If you had a rule engine that you could use to configure this instead, engineers would not be required.</p><p>A Rule could be created that:</p><ul><li><p>had the Trigger of <code>view</code></p></li><li><p>had a Condition of <code>view_count</code> being greater than <code>100</code></p></li><li><p>had an Effect of setting a field of the user to <code>popular</code></p></li></ul><p>The record would then run automatically through the rules engine, and could also be changed in the future.</p><h4>Sample code</h4><p>Having a hard time conceptualizing this? Not to fear&#8202;&#8212;&#8202;I&#8217;ve created a small Github repository to illustrate some of the concepts here.</p><p><strong><a href="https://github.com/jgefroh/rules-engine">JGefroh/rules-engine</a></strong><a href="https://github.com/jgefroh/rules-engine"><br></a><em><a href="https://github.com/jgefroh/rules-engine">This small library demonstrates a system capable of operating as a dynamic rules engine. You can read more in my&#8230;</a></em><a href="https://github.com/jgefroh/rules-engine">github.com</a></p><p>Note that this isn&#8217;t intended to be a production-grade system, but rather a simplified illustration of the above concepts. Actual rules engines have many more moving parts and take into consideration a whole host of other cases.</p><p>As a final disclaimer: improperly implemented rules engines or rules engines applied outside of the appropriate context can also make your system incredibly complicated and lead to a loss of organizational agility, so be judicious in their usage.</p><p>Rules engines are highly useful tools&#8202;&#8212;&#8202;in the right context. If implemented properly, they can significantly ease development burden and improve the agility of other departments within your organization.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QIih!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe514a521-16ce-42fc-bbac-0ef09f2b3dbc_250x250.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QIih!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe514a521-16ce-42fc-bbac-0ef09f2b3dbc_250x250.png 424w, https://substackcdn.com/image/fetch/$s_!QIih!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe514a521-16ce-42fc-bbac-0ef09f2b3dbc_250x250.png 848w, https://substackcdn.com/image/fetch/$s_!QIih!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe514a521-16ce-42fc-bbac-0ef09f2b3dbc_250x250.png 1272w, https://substackcdn.com/image/fetch/$s_!QIih!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe514a521-16ce-42fc-bbac-0ef09f2b3dbc_250x250.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QIih!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe514a521-16ce-42fc-bbac-0ef09f2b3dbc_250x250.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e514a521-16ce-42fc-bbac-0ef09f2b3dbc_250x250.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!QIih!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe514a521-16ce-42fc-bbac-0ef09f2b3dbc_250x250.png 424w, https://substackcdn.com/image/fetch/$s_!QIih!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe514a521-16ce-42fc-bbac-0ef09f2b3dbc_250x250.png 848w, https://substackcdn.com/image/fetch/$s_!QIih!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe514a521-16ce-42fc-bbac-0ef09f2b3dbc_250x250.png 1272w, https://substackcdn.com/image/fetch/$s_!QIih!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe514a521-16ce-42fc-bbac-0ef09f2b3dbc_250x250.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div>]]></content:encoded></item><item><title><![CDATA[How to Design Software — Image Uploaders]]></title><description><![CDATA[Learn how to design a scalable image uploading system!]]></description><link>https://blog.jgefroh.com/p/software-architecture-image-uploading-67997101a034</link><guid isPermaLink="false">https://blog.jgefroh.com/p/software-architecture-image-uploading-67997101a034</guid><dc:creator><![CDATA[Joseph Gefroh]]></dc:creator><pubDate>Sat, 19 Oct 2019 20:25:26 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/16b7e05a-642e-4648-b0b8-50b74c877589_800x418.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2ZrN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0a18f7a-7da5-4ec8-802f-2b7084e39451_800x418.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2ZrN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0a18f7a-7da5-4ec8-802f-2b7084e39451_800x418.png 424w, https://substackcdn.com/image/fetch/$s_!2ZrN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0a18f7a-7da5-4ec8-802f-2b7084e39451_800x418.png 848w, https://substackcdn.com/image/fetch/$s_!2ZrN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0a18f7a-7da5-4ec8-802f-2b7084e39451_800x418.png 1272w, https://substackcdn.com/image/fetch/$s_!2ZrN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0a18f7a-7da5-4ec8-802f-2b7084e39451_800x418.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2ZrN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0a18f7a-7da5-4ec8-802f-2b7084e39451_800x418.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e0a18f7a-7da5-4ec8-802f-2b7084e39451_800x418.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!2ZrN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0a18f7a-7da5-4ec8-802f-2b7084e39451_800x418.png 424w, https://substackcdn.com/image/fetch/$s_!2ZrN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0a18f7a-7da5-4ec8-802f-2b7084e39451_800x418.png 848w, https://substackcdn.com/image/fetch/$s_!2ZrN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0a18f7a-7da5-4ec8-802f-2b7084e39451_800x418.png 1272w, https://substackcdn.com/image/fetch/$s_!2ZrN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0a18f7a-7da5-4ec8-802f-2b7084e39451_800x418.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>In many startups I&#8217;ve worked with, image uploading was a part of their web application&#8217;s workflow. From user avatars to uploadable inventory pictures, it was a common-enough feature to be present in almost every system.</p><p>Rather unsurprisingly, many of those startups&#8217; solutions to their uploading suffered from the same issues. Logic to handle image uploads was ad-hoc. File processing happened at the same time as the request, causing the application server&#8217;s request queue to back up.</p><p>In short, it wasn&#8217;t so much a system as a bunch of disparate workflows cobbled together (typical of most startups). The result? A lot of bugs in image handling, mysterious crashes that can&#8217;t be traced, and random timeouts.</p><p>I&#8217;m here to show you a better way.</p><h3>Understanding the technical concerns</h3><p>Image uploading can be complex, and use-cases vary depending on the system.</p><p>There&#8217;s a lot of concerns with images in general that most people don&#8217;t think of when they are diving into building it out.</p><p>Images in a web application can easily touch on the following technical concerns:</p><ul><li><p>Displaying the image on the front-end</p></li><li><p>Authorizing users to download the image</p></li><li><p>Authorizing users to upload the image</p></li><li><p>Uploading the image to your server in a scalable way</p></li><li><p>Validating the image data</p></li><li><p>Processing the image, performing cropping, optimization, and other tasks</p></li><li><p>Creating image variants, such as banners and thumbnails</p></li><li><p>Storing the images</p></li><li><p>Associating the images to whatever records you&#8217;re uploading them for (such as user avatars or campaign banners).</p></li></ul><p>While not all of these would be present, a solid system will be able to flex and adapt to support these use cases with minimal changes.</p><h3>The Tradeoffs</h3><p>When thinking of a solution that can address these concerns, it quickly becomes evident that the final solution will be more complex than adding a column to your user model and an endpoint to upload the image, and calling it a day.</p><p>It&#8217;ll be helpful to dive into the tradeoffs to consider in a solution.</p><h4>Scaling</h4><p>Image uploads are notorious for crashing servers or causing timeouts. If a user attempts to upload a 10 megabyte image, that&#8217;s a lot of resource usage:</p><ul><li><p>10 megabytes of your server&#8216;s memory tied up</p></li><li><p>A request handler being tied up for the entire amount of time it takes to upload 10 megabytes</p></li><li><p>CPU usage to deal with the image upload</p></li></ul><p>If you&#8217;re dealing with 1 or 10 users uploading images, it&#8217;s not a big deal. However, if your system is actually used by users, it&#8217;ll quickly go out of control.</p><p>As a result, the architecture is required to be focused on reducing the amount of time your server is actually handling an image uploading request to almost nothing, and offloading the actual upload to another service (such as S3 or an in-house service dedicated to uploads). You don&#8217;t want image uploading taking up all of your web server&#8217;s capacity.</p><h4>Security</h4><p>Image uploads and processing are a massive source of security holes. Any endpoint that lets you tie up a massive amount of resources is vulnerable to denial-of-service attacks (intentional or not).</p><p>On an even more worrisome note, image processing is a poorly understood aspect of engineering that has led to some <a href="https://imagetragick.com/">tragic security flaws</a>, providing random users the ability to execute arbitrary commands as a root-level user.</p><p>Our system architecture has to handle images in a secure way that promotes availability but also provides integrity of user data.</p><h4>Authorization</h4><p>On the security front, there&#8217;s also business logic specific to our system. Not all images should be publicly available. Perhaps there are situations where users should not be able to download images uploaded by other users. Perhaps there might be rules surrounding who can upload an image.</p><p>Our system has to be able to handle these domain-specific authorization cases easily.</p><h4>Consistency</h4><p>I&#8217;ve written about the <a href="https://medium.com/@jgefroh/why-consistency-is-one-of-the-top-indicators-of-good-code-352ba5d62020">value of consistency</a> in the past. We don&#8217;t want a different way to upload an image for every kind of image we want to upload. Image upload use-cases like user avatars and campaign banners should all flow through the same image uploading workflow and should require minimal or no code change to support.</p><h4>Variants</h4><p>When an image is uploaded, there might be multiple places and ways it is used. Places like thumbnails, backgrounds, and profile images might use the same image in different ways. We might serve a lower resolution image to mobile users to save on bandwidth.</p><p>Whatever the case, we have to support the creation and usage of variants in our upload system. Creating variants can take a lot of processing power, and it&#8217;s not something we want our primary web server to do. The architecture has to offload that to a separate, asynchronous service.</p><h4>Growth</h4><p>All subsystems should be able to grow independently of the rest of the system. You never know when you&#8217;ll see an increased demand in usage. By keeping well defined boundaries in your system&nbsp;, you can easily convert them into micro-services or a separate deployment that scales horizontally.</p><h3>The Architecture</h3><p>With these trade-offs in mind, let&#8217;s now take a look at an architecture for uploading images that fulfills the criteria.</p><div class="paywall-jump" data-component-name="PaywallToDOM"></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Z7Ju!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3160cc46-6088-4a3b-9c2f-c5777e7f7f14_800x401.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Z7Ju!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3160cc46-6088-4a3b-9c2f-c5777e7f7f14_800x401.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Z7Ju!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3160cc46-6088-4a3b-9c2f-c5777e7f7f14_800x401.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Z7Ju!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3160cc46-6088-4a3b-9c2f-c5777e7f7f14_800x401.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Z7Ju!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3160cc46-6088-4a3b-9c2f-c5777e7f7f14_800x401.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Z7Ju!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3160cc46-6088-4a3b-9c2f-c5777e7f7f14_800x401.jpeg" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3160cc46-6088-4a3b-9c2f-c5777e7f7f14_800x401.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Z7Ju!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3160cc46-6088-4a3b-9c2f-c5777e7f7f14_800x401.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Z7Ju!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3160cc46-6088-4a3b-9c2f-c5777e7f7f14_800x401.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Z7Ju!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3160cc46-6088-4a3b-9c2f-c5777e7f7f14_800x401.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Z7Ju!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3160cc46-6088-4a3b-9c2f-c5777e7f7f14_800x401.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">A general architecture for uploading images.</figcaption></figure></div><h3>The Components</h3><p>What are each of the parts of the architecture intended for?</p><p>The various little nuances and decision points in the architecture provide significant advantages to scalability, security, and flexibility.</p><p>But, what are those nuances? What is gained or lost by each decision point? Why are we choosing to use signed URLs or other elements? Let&#8217;s dive into the details.</p><h4>Image Upload&nbsp;API</h4><p>The image upload API handles the lifecycle of an image upload request. It&#8217;ll perform authentication, authorization, auditing, rate limiting, and other items, but it&#8217;ll leave the actual management of the file data to other services.</p><p>It never touches file data. I leave it as an exercise to the reader to find ways to make it RESTful.</p><h4>Image Upload&nbsp;Service</h4><p>The Image Upload Service encapsulates the logic of calling the endpoints in the Image Upload API with the right data in the right order, hiding it behind a single method interface. This temporal coupling is not something you want spread out through your entire system, so having a single source of truth for this algorithm is incredibly important.</p><h4>Signed URLs</h4><p>A signed URL is a URL that has authorization parameters in it. We can use signed URLs for uploading and downloaded to provide protection and ensure that only authorized users perform these actions.</p><p><strong>Signed Upload URLs<br></strong>In a simple case,<code>/images/get_upload_url</code> could return an unsigned URL: <code>/uploads</code>.</p><p>However, this means that anyone could just call <code>/uploads</code> and fill our data store with random files or use it as their personal file server. This is clearly not desirable.</p><p><code>/images/get_upload_url</code> could return a signed URL:<br><code>/uploads?write_token=a99Xioajksf23</code>.</p><p>This means that we have an opportunity to authenticate the user, authorize them, rate limit, or audit who generated the signed URL. If someone abuses our upload service, we have the means to stop that user.</p><p><strong>Signed Download URLs<br></strong>If we didn&#8217;t have signed download URLs, anyone could download the image if they knew the URL: <code>/images/joseph-gefroh.png</code></p><p>This may be desirable in many cases, but in some cases, such as private files, it may be highly undesirable. In these cases, we wouldn&#8217;t want the URL to be publicly available.</p><p>We can gate access behind our own endpoint.&nbsp;<br><code>images/get_download_url</code>, which returns a temporary token such as:<br><code>/downloads?read_token=a99Xioajksf23</code>.</p><p>Just like uploads, this provides us an opportunity to authenticate, authorize, limit, and audit user access to the images.</p><h4><strong>Cloud File&nbsp;Storage</strong></h4><p>We use a separate file storage service (such as S3) to reduce the burden on our web servers.</p><p><strong>Upload endpoint<br></strong>File uploads can take a long time and can be resource-intensive. By offloading such operations to a service dedicated to this, we can ensure the rest of our system continues operating smoothly.</p><p><strong>Download endpoint<br></strong>We also never serve the files directly from our web server for the same reasons listed above&#8202;&#8212;&#8202;we instead deliver it from the external file store. Any request to our server instead returns another URL or redirects to the appropriate service.</p><h4>Image Processing Job</h4><p>Image processing can be highly memory and CPU intensive&#8202;&#8212;&#8202;it&#8217;s not something you want to perform on your application or web server, which is busy handling other requests.</p><p>We turn this into an asynchronous call that is offloaded to another service so that we can keep our primary web server unblocked and performant.</p><h4>Image Metadata&nbsp;Record</h4><p>We store the metadata of the image record in our database because we have to track it. There&#8217;s no sense uploading an image without a way to retrieve or manage it later. What use is uploading a photo intended to be used as a user avatar if we have no way to associate with the user record in question?</p><p>It&#8217;s important to note that we do not store the actual image itself in our database, we merely store the metadata, which we then use later to construct the various URLs to it.</p><p>Why don&#8217;t we store the image URL? Storing the image URL is fragile, and the link is susceptible to breaking if the URL ever changes for any reason. Storing the metadata needed to construct the URL is a lot more robust&#8202;&#8212;&#8202;we can easily change things like the domain name, and build the appropriate URL without having any downtime.</p><h3>The Algorithm</h3><p>Let&#8217;s examine how we would use the components in our system to actually upload the image:</p><ul><li><p>Step 1: Client request an upload URL from the server (REQUEST)</p></li><li><p>Step 2: Client uploads the image data to the upload URL (UPLOAD)</p></li><li><p>Step 3: Client tells the server the upload is completed (CONFIRM)</p></li><li><p>Step 4: Server processes image in background (PROCESS)</p></li><li><p>Step 5: Client checks image processing status (CHECK)</p></li><li><p>Step 6: Server is done processing image, notifies client (FINALIZE)</p></li></ul><h4>Step 1: Client request an upload URL from the server (REQUEST)</h4><p>Why is the first step not an image upload? Remember&#8202;&#8212;&#8202;we don&#8217;t want the image data to ever even touch our servers. It is far too much of a resource hog and denial-of-service vulnerability.</p><p>Instead, we ask our server to give use the URL we should upload to. This URL could point to a 3rd-party cloud storage, such as S3, or another system specifically built to handle the load of image uploading.</p><p>During this step, the server can generate a random URL that is:</p><ul><li><p>time restricted</p></li><li><p>audited</p></li><li><p>authorized</p></li></ul><p>These upload URLs are pre-signed URLs. That is, the URL the server returns has query parameters that indicate all of the authorization in it required to upload to the 3rd-party service.</p><p>After performing authorization checks, the server also creates a record in the database to track this individual image upload, with data on:</p><ul><li><p>the name of the file</p></li><li><p>the type of the file</p></li><li><p>the URL of the file</p></li><li><p>the status (eg. <code>requested</code>, <code>uploaded</code>, <code>processed</code>)</p></li><li><p>the associations of the image (eg. <code>user</code>, <code>campaign</code>)</p></li><li><p>the kind of association (eg. <code>banner</code>, <code>avatar</code>)</p></li><li><p>write token (a generated token that must be provided to modify the image)</p></li><li><p>read token (a generated token that must be provided to read the image)</p></li><li><p>any other audit data</p></li></ul><p>The server returns this data to the client.</p><h4>Step 2: Client uploads the image data to the upload URL&nbsp;(UPLOAD)</h4><p>This is a fairly straightforward step.</p><p>The client, now armed with the Upload URL from the server, simply performs a POST request to that URL. That service then accepts that data and stores it.</p><h4>Step 3: Client tells the server the upload is completed (CONFIRM)</h4><p>The client makes a request to the server with the token the server returned earlier, telling the server that the upload was completed.</p><h4>Step 4: Server processes image in background (PROCESS)</h4><p>The server verifies the token, and then checks for that upload request.</p><p>It kicks off a job that will process the image&#8202;&#8212;&#8202;verifying file integrity, creating variants, and performing optimizations without blocking requests to the web server.</p><h4>Step 5: Client checks image processing status&nbsp;(CHECK)</h4><p>Processing images takes some time, and you don&#8217;t want the client blocking a request. The client should check back occasionally to see if the processing is done.</p><h4>Step 6: Server is done processing image, notifies client (FINALIZE)</h4><p>Eventually, the check will pass and server is going to return the image URL. Now, the client is free to use the image.</p><p>The security here is provided by the image URL. If the image is protected, the URL to access the image would point to our server, and any request by the client would have to provide a read token, which the server could use to perform authorization as needed and generate a temporary signed URL for that image.</p><p>If the image is meant to be public, the image URL could be a direct reference to the image, bypassing our server altogether.</p><h3>That&#8217;s it.</h3><p>Implemented, this image uploading system can scale significantly and handle a lot of potential use cases. Adding new images would be as simple as adding a few lines of code.</p><p>The complexity of the system would be hidden behind clean interfaces that have implementations that, once built, would rarely change.</p><p>Built correctly, it could also support general file uploading as well!</p><p>Did you like this article? Let me know in the comments, or connect with me on <a href="https://www.linkedin.com/in/jgefroh/">LinkedIn</a>!</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!b1FA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3fd881f-bed7-4053-889b-ee2f122b559b_250x250.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!b1FA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3fd881f-bed7-4053-889b-ee2f122b559b_250x250.png 424w, https://substackcdn.com/image/fetch/$s_!b1FA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3fd881f-bed7-4053-889b-ee2f122b559b_250x250.png 848w, https://substackcdn.com/image/fetch/$s_!b1FA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3fd881f-bed7-4053-889b-ee2f122b559b_250x250.png 1272w, https://substackcdn.com/image/fetch/$s_!b1FA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3fd881f-bed7-4053-889b-ee2f122b559b_250x250.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!b1FA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3fd881f-bed7-4053-889b-ee2f122b559b_250x250.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f3fd881f-bed7-4053-889b-ee2f122b559b_250x250.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!b1FA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3fd881f-bed7-4053-889b-ee2f122b559b_250x250.png 424w, https://substackcdn.com/image/fetch/$s_!b1FA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3fd881f-bed7-4053-889b-ee2f122b559b_250x250.png 848w, https://substackcdn.com/image/fetch/$s_!b1FA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3fd881f-bed7-4053-889b-ee2f122b559b_250x250.png 1272w, https://substackcdn.com/image/fetch/$s_!b1FA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3fd881f-bed7-4053-889b-ee2f122b559b_250x250.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div>]]></content:encoded></item><item><title><![CDATA[How to Design Software — Tags and Groups]]></title><description><![CDATA[A feature you often need in web applications is the ability to order, filter, group, or organize records based on some arbitrary&#8230;]]></description><link>https://blog.jgefroh.com/p/architecture-tagging-stuff-dcda3218880a</link><guid isPermaLink="false">https://blog.jgefroh.com/p/architecture-tagging-stuff-dcda3218880a</guid><dc:creator><![CDATA[Joseph Gefroh]]></dc:creator><pubDate>Thu, 22 Dec 2016 08:14:32 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/769b1be9-e51d-407f-a938-61cd4871337c_800x442.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h4>Learn how to design a tagging system and provide the ability to order, filter, group, or organize records based on some arbitrary properties.</h4><p>A feature you often need in web applications is the ability to order, filter, group, or organize records based on some arbitrary properties. In many situations, it&#8217;s simply called a &#8220;tag&#8221;, but the purpose and behavior is the same whether it&#8217;s called categories, metadata, groups, or descriptors.</p><p>Take a look at the storefront for the popular PC gaming platform <a href="http://store.steampowered.com/">STEAM</a>. STEAM allows users to add text tags to games. Users can add any tag to a game, marking it as a &#8220;strategy&#8221; game, &#8220;painful&#8221; to play, or even just &#8220;goat&#8221;. Users can use these tags to filter their search to only show similarly tagged games, making it much easier to find the games they want.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PKxv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575d225f-02f6-4d2f-ab66-5cbbf0c40558_800x442.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PKxv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575d225f-02f6-4d2f-ab66-5cbbf0c40558_800x442.png 424w, https://substackcdn.com/image/fetch/$s_!PKxv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575d225f-02f6-4d2f-ab66-5cbbf0c40558_800x442.png 848w, https://substackcdn.com/image/fetch/$s_!PKxv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575d225f-02f6-4d2f-ab66-5cbbf0c40558_800x442.png 1272w, https://substackcdn.com/image/fetch/$s_!PKxv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575d225f-02f6-4d2f-ab66-5cbbf0c40558_800x442.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PKxv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575d225f-02f6-4d2f-ab66-5cbbf0c40558_800x442.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/575d225f-02f6-4d2f-ab66-5cbbf0c40558_800x442.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!PKxv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575d225f-02f6-4d2f-ab66-5cbbf0c40558_800x442.png 424w, https://substackcdn.com/image/fetch/$s_!PKxv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575d225f-02f6-4d2f-ab66-5cbbf0c40558_800x442.png 848w, https://substackcdn.com/image/fetch/$s_!PKxv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575d225f-02f6-4d2f-ab66-5cbbf0c40558_800x442.png 1272w, https://substackcdn.com/image/fetch/$s_!PKxv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575d225f-02f6-4d2f-ab66-5cbbf0c40558_800x442.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a><figcaption class="image-caption">STEAM&#8217;s website showcasing tags</figcaption></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jp7L!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3f64f78-8979-4d34-ad9d-c7f702119546_800x522.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jp7L!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3f64f78-8979-4d34-ad9d-c7f702119546_800x522.png 424w, https://substackcdn.com/image/fetch/$s_!jp7L!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3f64f78-8979-4d34-ad9d-c7f702119546_800x522.png 848w, https://substackcdn.com/image/fetch/$s_!jp7L!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3f64f78-8979-4d34-ad9d-c7f702119546_800x522.png 1272w, https://substackcdn.com/image/fetch/$s_!jp7L!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3f64f78-8979-4d34-ad9d-c7f702119546_800x522.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jp7L!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3f64f78-8979-4d34-ad9d-c7f702119546_800x522.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f3f64f78-8979-4d34-ad9d-c7f702119546_800x522.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!jp7L!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3f64f78-8979-4d34-ad9d-c7f702119546_800x522.png 424w, https://substackcdn.com/image/fetch/$s_!jp7L!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3f64f78-8979-4d34-ad9d-c7f702119546_800x522.png 848w, https://substackcdn.com/image/fetch/$s_!jp7L!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3f64f78-8979-4d34-ad9d-c7f702119546_800x522.png 1272w, https://substackcdn.com/image/fetch/$s_!jp7L!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3f64f78-8979-4d34-ad9d-c7f702119546_800x522.png 1456w" sizes="100vw"></picture><div></div></div></a><figcaption class="image-caption">A diagram of what tags might look&nbsp;like.</figcaption></figure></div><p>Let&#8217;s take a look at several ways to approach tagging.</p><h3>The Naive&nbsp;Approach</h3><p>The naive approach you can do is to store the tags in as a comma separated string within a single database column, as shown below:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rzVS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb0a4f80-c940-4f0c-b6ee-0b0096a40052_758x639.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rzVS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb0a4f80-c940-4f0c-b6ee-0b0096a40052_758x639.png 424w, https://substackcdn.com/image/fetch/$s_!rzVS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb0a4f80-c940-4f0c-b6ee-0b0096a40052_758x639.png 848w, https://substackcdn.com/image/fetch/$s_!rzVS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb0a4f80-c940-4f0c-b6ee-0b0096a40052_758x639.png 1272w, https://substackcdn.com/image/fetch/$s_!rzVS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb0a4f80-c940-4f0c-b6ee-0b0096a40052_758x639.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rzVS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb0a4f80-c940-4f0c-b6ee-0b0096a40052_758x639.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/db0a4f80-c940-4f0c-b6ee-0b0096a40052_758x639.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!rzVS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb0a4f80-c940-4f0c-b6ee-0b0096a40052_758x639.png 424w, https://substackcdn.com/image/fetch/$s_!rzVS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb0a4f80-c940-4f0c-b6ee-0b0096a40052_758x639.png 848w, https://substackcdn.com/image/fetch/$s_!rzVS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb0a4f80-c940-4f0c-b6ee-0b0096a40052_758x639.png 1272w, https://substackcdn.com/image/fetch/$s_!rzVS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb0a4f80-c940-4f0c-b6ee-0b0096a40052_758x639.png 1456w" sizes="100vw"></picture><div></div></div></a></figure></div><h4>PRO: It&#8217;s dead&nbsp;simple.</h4><p>Let&#8217;s face it. Adding a single column is really the easiest thing you can do.</p><h4>CON: Querying is a&nbsp;pain.</h4><p>How do you find potatoes that are delicious and warm, but not buttery or just delicious or just warm? Writing the query to do that would be difficult and cumbersome, especially as the number of possible tags increased.</p><h4>CON: Prone to formatting errors</h4><p>If a user adds a comma to their tag, you suddenly have a problem&#8202;&#8212;&#8202;that tag gets split into two tags.</p><h4>CON: Limited by field&nbsp;length</h4><p>Database columns have a maximum length and you might hit it.</p><h3>Separate Columns</h3><p>An evolution of the first approach is to store each tag of a record in its own column, conveniently labeled <code>tag1</code>, <code>tag2</code>, <code>tag3</code>, and so on.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lWRF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F063d301f-32d5-4f05-840d-3e6fa007ae8d_800x741.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lWRF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F063d301f-32d5-4f05-840d-3e6fa007ae8d_800x741.png 424w, https://substackcdn.com/image/fetch/$s_!lWRF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F063d301f-32d5-4f05-840d-3e6fa007ae8d_800x741.png 848w, https://substackcdn.com/image/fetch/$s_!lWRF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F063d301f-32d5-4f05-840d-3e6fa007ae8d_800x741.png 1272w, https://substackcdn.com/image/fetch/$s_!lWRF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F063d301f-32d5-4f05-840d-3e6fa007ae8d_800x741.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lWRF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F063d301f-32d5-4f05-840d-3e6fa007ae8d_800x741.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/063d301f-32d5-4f05-840d-3e6fa007ae8d_800x741.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lWRF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F063d301f-32d5-4f05-840d-3e6fa007ae8d_800x741.png 424w, https://substackcdn.com/image/fetch/$s_!lWRF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F063d301f-32d5-4f05-840d-3e6fa007ae8d_800x741.png 848w, https://substackcdn.com/image/fetch/$s_!lWRF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F063d301f-32d5-4f05-840d-3e6fa007ae8d_800x741.png 1272w, https://substackcdn.com/image/fetch/$s_!lWRF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F063d301f-32d5-4f05-840d-3e6fa007ae8d_800x741.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h4>PRO: It looks&nbsp;right.</h4><p>Having multiple columns for multiple values seems to be following sound database practices&#8230;right? (not in this case).</p><h4>CON: Querying might actually be&nbsp;harder.</h4><p>It might look like a good idea on the surface, but querying is actually harder. Now you have to include every single column in the query. Additionally, your code would have to check for whether a tag existed in every possible tag column since there&#8217;s no guarantee that a tag would be in any specific column for any record. <code>tag1</code> might have &#8220;buttery&#8221; in Record 1, but Record 2 might have &#8220;warm&#8221; in <code>tag1</code> and &#8220;buttery&#8221; in <code>tag2</code>.</p><div class="paywall-jump" data-component-name="PaywallToDOM"></div><h4>CON: Adding more tags is difficult.</h4><p>Every time you wanted to add more tags, you would need to add a new database column and change all your queries in the process. Just like the naive approach, you&#8217;d reach the limit pretty quickly.</p><h3>The Separate&nbsp;Model</h3><p>A third approach is to create a separate model for the tag. If you&#8217;re tagging a <code>Potato</code>, for example, you would create a model called <code>PotatoTag</code> that had a reference to the original <code>Potato</code> via a <code>potato_id</code> foreign key.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YpI_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3610065-4639-4157-a069-175fdf187be0_800x457.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YpI_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3610065-4639-4157-a069-175fdf187be0_800x457.png 424w, https://substackcdn.com/image/fetch/$s_!YpI_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3610065-4639-4157-a069-175fdf187be0_800x457.png 848w, https://substackcdn.com/image/fetch/$s_!YpI_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3610065-4639-4157-a069-175fdf187be0_800x457.png 1272w, https://substackcdn.com/image/fetch/$s_!YpI_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3610065-4639-4157-a069-175fdf187be0_800x457.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YpI_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3610065-4639-4157-a069-175fdf187be0_800x457.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a3610065-4639-4157-a069-175fdf187be0_800x457.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!YpI_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3610065-4639-4157-a069-175fdf187be0_800x457.png 424w, https://substackcdn.com/image/fetch/$s_!YpI_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3610065-4639-4157-a069-175fdf187be0_800x457.png 848w, https://substackcdn.com/image/fetch/$s_!YpI_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3610065-4639-4157-a069-175fdf187be0_800x457.png 1272w, https://substackcdn.com/image/fetch/$s_!YpI_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3610065-4639-4157-a069-175fdf187be0_800x457.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h4>PRO: Querying is easier than the naive&nbsp;approach</h4><p>In order to find all potatoes with the tag &#8220;buttery&#8221;, you would simply query for any potato with a <code>PotatoTag</code> that said &#8220;buttery&#8221;.</p><h4>PRO: No practical limit</h4><p>Unlike the naive approach, you don&#8217;t have a practical limit to the number of tags any one record could have.</p><h4>CON: Separate model for every kind of&nbsp;tag</h4><p><code>PotatoTag</code>, <code>GameTag</code>, <code>SnailTag</code>&#8202;&#8212;&#8202;every kind of model you have would require its own table, which would get out of hand very quickly.</p><h3>The Polymorphic Model</h3><p>Instead of having a separate model for every tag, you have a generic model that is polymorphic.</p><p>Polymorphic database models essentially means that it can reference tables it doesn&#8217;t know about with the help of an extra field. In a typical relationship you would store the primary key of a reference (like the numeric id). However, with a polymorphic association, you would also store the type of the object associated with the id (eg. <code>taggable_id</code> and <code>taggable_type</code>). This allows you to associate records with multiple kinds of objects, even if they share the same id but are different types.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5ZIN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff752640a-dfb2-48c1-82b7-7cc5d5e7c8e7_800x504.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5ZIN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff752640a-dfb2-48c1-82b7-7cc5d5e7c8e7_800x504.png 424w, https://substackcdn.com/image/fetch/$s_!5ZIN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff752640a-dfb2-48c1-82b7-7cc5d5e7c8e7_800x504.png 848w, https://substackcdn.com/image/fetch/$s_!5ZIN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff752640a-dfb2-48c1-82b7-7cc5d5e7c8e7_800x504.png 1272w, https://substackcdn.com/image/fetch/$s_!5ZIN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff752640a-dfb2-48c1-82b7-7cc5d5e7c8e7_800x504.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5ZIN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff752640a-dfb2-48c1-82b7-7cc5d5e7c8e7_800x504.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f752640a-dfb2-48c1-82b7-7cc5d5e7c8e7_800x504.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!5ZIN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff752640a-dfb2-48c1-82b7-7cc5d5e7c8e7_800x504.png 424w, https://substackcdn.com/image/fetch/$s_!5ZIN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff752640a-dfb2-48c1-82b7-7cc5d5e7c8e7_800x504.png 848w, https://substackcdn.com/image/fetch/$s_!5ZIN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff752640a-dfb2-48c1-82b7-7cc5d5e7c8e7_800x504.png 1272w, https://substackcdn.com/image/fetch/$s_!5ZIN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff752640a-dfb2-48c1-82b7-7cc5d5e7c8e7_800x504.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">A Taggable could be a potato, an event, or even other&nbsp;tags.</figcaption></figure></div><h4>PRO: Benefits of Separate Model&nbsp;Approach</h4><p>This approach shares the benefits of the separate model approach but without the maintenance burden of actually having separate models.</p><h4>PRO: Tag anything and use the same exact&nbsp;code</h4><p>Polymorphic models let you stick to the Don&#8217;t Repeat Yourself (DRY) principle. The code to interact with tags is exactly the same no matter what kind of record you&#8217;re tagging.</p><h3>The Polymorphic Model with Constraints</h3><p>Let&#8217;s say you don&#8217;t want users to be able to create arbitrary tags, but rather select from a limited set of tags. You could easily enforce this rule in the backend code. Let&#8217;s add a twist and say that you don&#8217;t know ahead of time what tags are in that set of allowed tags. This could happen in situations where administrators could choose what tags are available.</p><p>You&#8217;d need to allow administrators to store the allowed tags, and then draw from that list of allowed tags when normal users want to tag a record. A model where the tag information lies in a <code>TagOption</code> instead of in the tag itself can support this situation. The <code>Tag</code> itself would simply be an associative entity between the <code>Taggable</code> record and the <code>TagOption</code>, with the <code>TagOption</code> itself having the actual information of the tag.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!sftW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ffc2104-dd7e-4c17-9341-73dbde515c44_800x379.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sftW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ffc2104-dd7e-4c17-9341-73dbde515c44_800x379.png 424w, https://substackcdn.com/image/fetch/$s_!sftW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ffc2104-dd7e-4c17-9341-73dbde515c44_800x379.png 848w, https://substackcdn.com/image/fetch/$s_!sftW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ffc2104-dd7e-4c17-9341-73dbde515c44_800x379.png 1272w, https://substackcdn.com/image/fetch/$s_!sftW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ffc2104-dd7e-4c17-9341-73dbde515c44_800x379.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!sftW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ffc2104-dd7e-4c17-9341-73dbde515c44_800x379.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4ffc2104-dd7e-4c17-9341-73dbde515c44_800x379.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!sftW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ffc2104-dd7e-4c17-9341-73dbde515c44_800x379.png 424w, https://substackcdn.com/image/fetch/$s_!sftW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ffc2104-dd7e-4c17-9341-73dbde515c44_800x379.png 848w, https://substackcdn.com/image/fetch/$s_!sftW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ffc2104-dd7e-4c17-9341-73dbde515c44_800x379.png 1272w, https://substackcdn.com/image/fetch/$s_!sftW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ffc2104-dd7e-4c17-9341-73dbde515c44_800x379.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h4>PRO: Limit tags to pre-approved set.</h4><p>This approach lets you limit tags to an arbitrary set of tags.</p><h4>PRO: Globally change&nbsp;tags</h4><p>Because the actual tag information lives in the <code>TagOption</code>, you can change the meaning of a tag without changing the models associated with it. For example, if you had a tag option called &#8220;orange&#8221; and you wanted to change it to say &#8220;Orange&#8221;, you could by changing a single record (the <code>TagOption</code>)instead of every <code>Tag</code> in the database.</p><p>These are a few of the ways you could design a tag feature in your system. Remember that a tag can be anything, not just a word or name! It could be a color, a priority, or really any discrete value you can use to categorize or group records.</p>]]></content:encoded></item></channel></rss>